Repository: objective-see/LuLu Branch: master Commit: a94454383c84 Files: 171 Total size: 1.6 MB Directory structure: gitextract_0dvkl5nv/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── DMG/ │ ├── LuLu.icns │ └── createDMG.sh ├── LICENSE.md ├── LuLu/ │ ├── App/ │ │ ├── 3rd-party/ │ │ │ ├── OrderedDictionary.h │ │ │ └── OrderedDictionary.m │ │ ├── AboutWindowController.h │ │ ├── AboutWindowController.m │ │ ├── AddRuleWindowController.h │ │ ├── AddRuleWindowController.m │ │ ├── AlertWindowController.h │ │ ├── AlertWindowController.m │ │ ├── App.entitlements │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets/ │ │ │ ├── Ancestry.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── FriendsFleet.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsHuntress.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsJamf.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsKandji.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsMacPaw.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsMalwarebytes.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsPANW.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsSophos.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── FriendsiVerify.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Heart.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Icon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── InstallAllow.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── InstallApprove.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── InstallApprove_OLD.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── InstallAuth.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── InstallBlocked.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── LuLuText.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── PrefsList.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── PrefsMode.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── PrefsProfiles.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── PrefsRules.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── PrefsUpdate.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── RulesAllow.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── RulesBlock.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── RulesSystem.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Signed.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── SignedApple.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── SignedUnknown.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── StatusActive.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── StatusInactive.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Unsigned.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── VTIcon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── allow.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── block.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── refresh.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── rulesRecent.imageset/ │ │ │ │ └── Contents.json │ │ │ └── rulesUnknown.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── AboutWindow.xib │ │ │ ├── AddRule.xib │ │ │ ├── AlertWindow.xib │ │ │ ├── ItemPaths.xib │ │ │ ├── MainMenu.xib │ │ │ ├── Preferences.xib │ │ │ ├── Rules.xib │ │ │ ├── StartupWindowController.xib │ │ │ ├── StatusBarPopover.xib │ │ │ ├── UpdateWindow.xib │ │ │ └── Welcome.xib │ │ ├── Configure.h │ │ ├── Configure.m │ │ ├── Extension.h │ │ ├── Extension.m │ │ ├── Info.plist │ │ ├── InfoPlist.xcstrings │ │ ├── ItemPathsWindowController.h │ │ ├── ItemPathsWindowController.m │ │ ├── NSApplicationKeyEvents.h │ │ ├── NSApplicationKeyEvents.m │ │ ├── ParentsWindowController.h │ │ ├── ParentsWindowController.m │ │ ├── PrefsWindowController.h │ │ ├── PrefsWindowController.m │ │ ├── RuleRow.h │ │ ├── RuleRow.m │ │ ├── RulesMenuController.h │ │ ├── RulesMenuController.m │ │ ├── RulesWindowController.h │ │ ├── RulesWindowController.m │ │ ├── SigningInfoViewController.h │ │ ├── SigningInfoViewController.m │ │ ├── StartupWindowController.h │ │ ├── StartupWindowController.m │ │ ├── StatusBarItem.h │ │ ├── StatusBarItem.m │ │ ├── StatusBarPopoverController.h │ │ ├── StatusBarPopoverController.m │ │ ├── Update.h │ │ ├── Update.m │ │ ├── UpdateWindowController.h │ │ ├── UpdateWindowController.m │ │ ├── WelcomeWindowController.h │ │ ├── WelcomeWindowController.m │ │ ├── XPCDaemonClient.h │ │ ├── XPCDaemonClient.m │ │ ├── XPCUser.h │ │ ├── XPCUser.m │ │ ├── instructions.psd │ │ ├── main.m │ │ ├── mul.lproj/ │ │ │ ├── AboutWindow.xcstrings │ │ │ ├── AddRule.xcstrings │ │ │ ├── AlertWindow.xcstrings │ │ │ ├── ItemPaths.xcstrings │ │ │ ├── MainMenu.xcstrings │ │ │ ├── Preferences.xcstrings │ │ │ ├── Rules.xcstrings │ │ │ ├── StartupWindowController.xcstrings │ │ │ ├── StatusBarPopover.xcstrings │ │ │ ├── UpdateWindow.xcstrings │ │ │ └── Welcome.xcstrings │ │ └── patrons.txt │ ├── Extension/ │ │ ├── Alerts.h │ │ ├── Alerts.m │ │ ├── Binary.h │ │ ├── Binary.m │ │ ├── BlockOrAllowList.h │ │ ├── BlockOrAllowList.m │ │ ├── Extension.entitlements │ │ ├── FilterDataProvider.h │ │ ├── FilterDataProvider.m │ │ ├── GrayList.h │ │ ├── GrayList.m │ │ ├── Info.plist │ │ ├── Preferences.h │ │ ├── Preferences.m │ │ ├── Process.h │ │ ├── Process.m │ │ ├── Profiles.h │ │ ├── Profiles.m │ │ ├── Rules.h │ │ ├── Rules.m │ │ ├── XPCDaemon.h │ │ ├── XPCDaemon.m │ │ ├── XPCListener.h │ │ ├── XPCListener.m │ │ ├── XPCUserClient.h │ │ ├── XPCUserClient.m │ │ ├── main.h │ │ ├── main.m │ │ └── procInfo.h │ ├── LuLu.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── Extension.xcscheme │ │ └── LuLu.xcscheme │ ├── Shared/ │ │ ├── Localizable.xcstrings │ │ ├── Rule.h │ │ ├── Rule.m │ │ ├── XPCDaemonProto.h │ │ ├── XPCUserProto.h │ │ ├── consts.h │ │ ├── signing.h │ │ ├── signing.m │ │ ├── utilities.h │ │ └── utilities.m │ └── Tests/ │ ├── README.md │ ├── run_passive_mode_tests.sh │ └── test_passive_mode_improvements.m ├── README.md ├── README_zh-Hans.md ├── README_zh-Hant.md └── lulu.xcworkspace/ ├── contents.xcworkspacedata └── xcshareddata/ └── IDEWorkspaceChecks.plist ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: objective-see patreon: objective_see ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## macOS .DS_Store ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xccheckout *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. Carthage/Build Carthage/Checkouts DMG/Release/* DMG/LuLu.app DMG/*.dmg DMG/background.psd LuLu/Binaries/ ================================================ FILE: DMG/createDMG.sh ================================================ VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "Release/LuLu.app/Contents/Info.plist") printf "\nCreating LuLu Disk Image...\n\n" create-dmg \ --volname "LuLu v$VERSION" \ --volicon "LuLu.icns" \ --background "background.png" \ --window-pos 200 120 \ --window-size 800 400 \ --icon-size 100 \ --icon "LuLu.app" 200 190 \ --hide-extension "LuLu.app" \ --app-drop-link 600 190 \ "LuLu_$VERSION.dmg" \ "Release/" printf "\nCode signing dmg...\n" codesign --force --sign "Developer ID Application: Objective-See, LLC (VBG97UB4TA)" LuLu_$VERSION.dmg printf "Done!\n" ================================================ FILE: LICENSE.md ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: LuLu/App/3rd-party/OrderedDictionary.h ================================================ // // OrderedDictionary.h // OrderedDictionary // // Created by Matt Gallagher on 19/12/08. // Copyright 2008 Matt Gallagher. All rights reserved. // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. Permission is granted to anyone to // use this software for any purpose, including commercial applications, and to // alter it and redistribute it freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source // distribution. // @import Cocoa; @interface OrderedDictionary : NSMutableDictionary { NSMutableDictionary *dictionary; NSMutableArray *array; } /* METHODS */ -(void)reverse; -(id)keyAtIndex:(NSUInteger)anIndex; -(void)insertObject:(id)anObject forKey:(id)aKey atIndex:(NSUInteger)anIndex; @end ================================================ FILE: LuLu/App/3rd-party/OrderedDictionary.m ================================================ // // OrderedDictionary.m // OrderedDictionary // // Created by Matt Gallagher on 19/12/08. // Copyright 2008 Matt Gallagher. All rights reserved. // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. Permission is granted to anyone to // use this software for any purpose, including commercial applications, and to // alter it and redistribute it freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source // distribution. // #import "Consts.h" #import "OrderedDictionary.h" NSString *DescriptionForObject(NSObject *object, id locale, NSUInteger indent) { NSString *objectString; if ([object isKindOfClass:[NSString class]]) { objectString = (NSString *)object; } else if ([object respondsToSelector:@selector(descriptionWithLocale:indent:)]) { objectString = [(NSDictionary *)object descriptionWithLocale:locale indent:indent]; } else if ([object respondsToSelector:@selector(descriptionWithLocale:)]) { objectString = [(NSSet *)object descriptionWithLocale:locale]; } else { objectString = [object description]; } return objectString; } @implementation OrderedDictionary -(id)init { self = [super init]; if (self != nil) { dictionary = [NSMutableDictionary dictionary]; array = [NSMutableArray array]; } return self; } -(id)copy { return [self mutableCopy]; } -(id)mutableCopy { OrderedDictionary* copy = nil; copy = [[OrderedDictionary alloc] init]; copy->array = [array mutableCopy]; copy->dictionary = [dictionary mutableCopy]; return copy; } -(void)reverse { [array setArray:[[array reverseObjectEnumerator] allObjects]]; } -(void)setObject:(id)anObject forKey:(id)aKey { if(![dictionary objectForKey:aKey]) { // [array addObject:aKey]; } [dictionary setObject:anObject forKey:aKey]; } -(void)removeObjectForKey:(id)aKey { [dictionary removeObjectForKey:aKey]; [array removeObject:aKey]; } - (NSUInteger)count { return [dictionary count]; } -(id)objectForKey:(id)aKey { return [dictionary objectForKey:aKey]; } -(NSEnumerator *)keyEnumerator { return [array objectEnumerator]; } -(void)insertObject:(id)anObject forKey:(id)aKey atIndex:(NSUInteger)anIndex { //remove old object if([dictionary objectForKey:aKey]) { //remove [self removeObjectForKey:aKey]; } //adding at end? // just append item if(array.count == anIndex) { //add [array addObject:aKey]; } //insert object else { //insert [array insertObject:aKey atIndex:anIndex]; } //add to dictionary [dictionary setObject:anObject forKey:aKey]; } -(id)keyAtIndex:(NSUInteger)anIndex { //object id item = nil; if((nil == array) || (anIndex >= array.count)) { //bail goto bail; } //extract item item = [array objectAtIndex:anIndex]; bail: return item; } @end ================================================ FILE: LuLu/App/AboutWindowController.h ================================================ // // file: AboutWindowController.h // project: lulu (main app) // description: 'about' window controller (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @interface AboutWindowController : NSWindowController { } /* PROPERTIES */ //version label/string @property (weak) IBOutlet NSTextField *versionLabel; //patrons @property (unsafe_unretained) IBOutlet NSTextView *patrons; /* METHODS */ //automatically invoked when user clicks any of the buttons // ->perform actions, such as loading patreon or products URL -(IBAction)buttonHandler:(id)sender; @end ================================================ FILE: LuLu/App/AboutWindowController.m ================================================ // // file: AboutWindowController.m // project: lulu (main app) // description: 'about' window controller // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "AboutWindowController.h" @implementation AboutWindowController @synthesize patrons; @synthesize versionLabel; //automatically called when nib is loaded // ->center window -(void)awakeFromNib { //center [self.window center]; } //automatically invoked when window is loaded // set to window to white, set app version, patrons, etc -(void)windowDidLoad { //version NSString* version = nil; //super [super windowDidLoad]; //not in dark mode? // make window white if(YES != isDarkMode()) { //make white self.window.backgroundColor = NSColor.whiteColor; } //grab app version version = getAppVersion(); if(nil == version) { //default version = NSLocalizedString(@"unknown", @"unknown"); } //set version sting self.versionLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Version: %@", @"Version: %@"), version]; //load patrons self.patrons.string = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"patrons" ofType:@"txt"] encoding:NSUTF8StringEncoding error:NULL]; if(nil == self.patrons.string) { //default self.patrons.string = NSLocalizedString(@"ERROR: failed to load patrons", @"ERROR: failed to load patrons"); } return; } //automatically invoked when window is closing // ->make window unmodal -(void)windowWillClose:(NSNotification *)notification { //make un-modal [[NSApplication sharedApplication] stopModal]; return; } //automatically invoked when user clicks any of the buttons // perform actions, such as loading patreon or products URL -(IBAction)buttonHandler:(id)sender { //support us button if(((NSButton*)sender).tag == BUTTON_SUPPORT_US) { //open URL // invokes user's default browser [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:PATREON_URL]]; } //more info button else if(((NSButton*)sender).tag == BUTTON_MORE_INFO) { //open URL // invokes user's default browser [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:PRODUCT_URL]]; } return; } @end ================================================ FILE: LuLu/App/AddRuleWindowController.h ================================================ // // file: AddRuleWindowController.h // project: lulu // description: 'add/edit rule' window controller (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "Rule.h" @interface AddRuleWindowController : NSWindowController /* PROPERTIES */ //app/binary icon @property (weak) IBOutlet NSImageView *icon; //path to app/binary @property (weak) IBOutlet NSTextField *path; //endpoint address @property (weak) IBOutlet NSTextField *endpointAddr; //but indicating endpoint addr is a regex @property (weak) IBOutlet NSButton *isEndpointAddrRegex; //endpoint port @property (weak) IBOutlet NSTextField *endpointPort; //'add' button @property (weak) IBOutlet NSButton *addButton; //block button @property (weak) IBOutlet NSButton *blockButton; //allow button @property (weak) IBOutlet NSButton *allowButton; //(existing) rule @property (nonatomic, retain)Rule* rule; //info (to create/update rule) @property(nonatomic, retain) NSDictionary* info; /* METHODS */ //'block'/'allow' button handler // just needed so buttons will toggle -(IBAction)radioButtonsHandler:(id)sender; //'browse' button handler // open a panel for user to select file -(IBAction)browseButtonHandler:(id)sender; //'cancel' button handler // returns NSModalResponseCancel -(IBAction)cancelButtonHandler:(id)sender; //'add' button handler // returns NSModalResponseOK -(IBAction)addButtonHandler:(id)sender; @end ================================================ FILE: LuLu/App/AddRuleWindowController.m ================================================ // // file: AddRuleWindowController.h // project: lulu // description: 'add/edit rule' window controller // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "AddRuleWindowController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation AddRuleWindowController @synthesize rule; //automatically called when nib is loaded // center window, set some defaults such as icon -(void)awakeFromNib { //set icon self.icon.image = [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode(kGenericApplicationIcon)]; //resize [self.icon.image setSize:NSMakeSize(128, 128)]; //set delegate for process path text field self.path.delegate = self; //existing rule? // prepopulate fields if(nil != self.rule) { //set title self.window.title = @"Edit Exiting Rule"; //path self.path.stringValue = self.rule.path; //endpoint addr self.endpointAddr.stringValue = self.rule.endpointAddr; //regex button if(YES == self.rule.isEndpointAddrRegex) { //on self.isEndpointAddrRegex.state = NSControlStateValueOn; } //endpoint port self.endpointPort.stringValue = self.rule.endpointPort; //block? if(RULE_STATE_BLOCK == self.rule.action.intValue) { //set self.blockButton.state = NSControlStateValueOn; } //allow? else { //set self.allowButton.state = NSControlStateValueOn; } //configure 'add' button self.addButton.title = NSLocalizedString(@"Update", @"Update"); //enable 'add' button self.addButton.enabled = YES; //make add/edit button first responder dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ //set first responder [self.window makeFirstResponder:self.addButton]; }); } //no rule // just init some defaults else { //'add' button should be disabled // ...until user enters an app/binary self.addButton.enabled = NO; } return; } //automatically called when editing start // update UI by resetting icon and disabling 'add' button -(void)controlTextDidBeginEditing:(NSNotification *)obj { //reset icon self.icon.image = [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode(kGenericApplicationIcon)]; //resize [self.icon.image setSize:NSMakeSize(128, 128)]; //disable 'add' button self.addButton.enabled = NO; return; } //automatically called when text changes // invoke helper to set icon, and enable/select 'add' button -(void)controlTextDidChange:(NSNotification *)notification { //ignore everything but process path if([notification object] != self.path) { //bail goto bail; } //update [self updateUI]; bail: return; } //automatically called when 'enter' is hit // invoke helper to set icon, and enable/select 'add' button -(void)controlTextDidEndEditing:(NSNotification *)notification { //ignore everything but process path if([notification object] != self.path) { //bail goto bail; } //update [self updateUI]; //make 'add' selected [self.window makeFirstResponder:self.addButton]; bail: return; } //'block'/'allow' button handler // just needed so buttons will toggle -(IBAction)radioButtonsHandler:(id)sender { return; } //'browse' button handler // open a panel for user to select file -(IBAction)browseButtonHandler:(id)sender { //'browse' panel NSOpenPanel *panel = nil; //response to 'browse' panel NSInteger response = 0; //init panel panel = [NSOpenPanel openPanel]; //allow files panel.canChooseFiles = YES; //allow directories (app bundles) panel.canChooseDirectories = YES; //can open app bundles panel.treatsFilePackagesAsDirectories = YES; //start in /Apps panel.directoryURL = [NSURL fileURLWithPath:@"/Applications"]; //disable multiple selections panel.allowsMultipleSelection = NO; //show it response = [panel runModal]; //ignore cancel if(NSModalResponseCancel == response) { //bail goto bail; } //set text self.path.stringValue = panel.URL.path; //update UI [self updateUI]; //make 'add' selected [self.window makeFirstResponder:self.addButton]; bail: return; } //'cancel' button handler // close sheet, returning NSModalResponseCancel -(IBAction)cancelButtonHandler:(id)sender { //dbg msg os_log_debug(logHandle, "user clicked: %{public}@", ((NSButton*)sender).title); //stop/cancel [NSApp stopModalWithCode:NSModalResponseCancel]; //close [self.window close]; return; } //'add' button handler // close sheet, returning NSModalResponseOK -(IBAction)addButtonHandler:(id)sender { //response NSModalResponse response = NSModalResponseAbort; //path NSMutableString* path = nil; //flag BOOL exists = NO; //flag BOOL isDirectory = NO; //(remote) endpoint addr NSString* endpointAddr = nil; //flag NSNumber* endpointAddrRegex = nil; //(remote) endpoint addr NSString* endpointPort = nil; //action NSNumber* action = nil; //error NSError* error = nil; //dbg msg os_log_debug(logHandle, "user clicked: %{public}@", ((NSButton*)sender).title); //init path // and check path = [self.path.stringValue mutableCopy]; if(0 == path.length) { //bail goto bail; } //set flags // exists/is directory exists = [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDirectory]; //if its a directory (but not bundle) // add '/*' to make it directory rule if( (YES == isDirectory) && (nil == [NSBundle bundleWithPath:path])) { //no '/'? // ...append it if(YES != [path hasSuffix:@"/"]) { //append [path appendString:@"/"]; } //add '*' to make it a directory rule [path appendString:VALUE_ANY]; } //invalid path? if( (YES != exists) || (0 == path.length) ) { //error // though only if its not '*' or '/*' if(YES != [path hasSuffix:VALUE_ANY]) { //show alert showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: invalid path", @"ERROR: invalid path"), [NSString stringWithFormat:NSLocalizedString(@"%@ does not exist!", @"%@ does not exist!"), path], @[NSLocalizedString(@"OK", @"OK")]); //bail goto bail; } } //endpoint addr // or '*' if blank endpointAddr = (0 != self.endpointAddr.stringValue.length) ? self.endpointAddr.stringValue : VALUE_ANY; //is endpoint addr regex? // ...set var, but also validate endpointAddrRegex = [NSNumber numberWithBool:(self.isEndpointAddrRegex.state == NSControlStateValueOn)]; if( (YES == endpointAddrRegex.boolValue) && (nil == [NSRegularExpression regularExpressionWithPattern:endpointAddr options:0 error:&error]) ) { //show alert showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: invalid regex", @"ERROR: invalid regex"), [NSString stringWithFormat:NSLocalizedString(@"%@ is not a valid regular expression\r\ndetails: %@", @"%@ is not a valid regular expression\r\ndetails: %@"), endpointAddr, error.localizedDescription], @[NSLocalizedString(@"OK", @"OK")]); //bail goto bail; } //endpoint port // ...set var, but also validate endpointPort = (0 != self.endpointPort.stringValue.length) ? self.endpointPort.stringValue : VALUE_ANY; if( (0 != self.endpointPort.stringValue.length) && (YES != [self.endpointPort.stringValue isEqualToString:VALUE_ANY]) && (NSNotFound != [endpointPort rangeOfCharacterFromSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]].location) ) { //show alert showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: invalid port", @"ERROR: invalid port"), [NSString stringWithFormat:NSLocalizedString(@"%@ is not a valid (port) number", @"%@ is not a valid (port) number"), endpointPort], @[NSLocalizedString(@"OK", @"OK")]); //bail goto bail; } //set action action = (self.blockButton.state == NSControlStateValueOn) ? @RULE_STATE_BLOCK : @RULE_STATE_ALLOW; //save all info self.info = @{KEY_PATH:path, KEY_ENDPOINT_ADDR:endpointAddr, KEY_ENDPOINT_ADDR_IS_REGEX:endpointAddrRegex, KEY_ENDPOINT_PORT:endpointPort, KEY_TYPE:@RULE_TYPE_USER, KEY_ACTION:action}; //ok happy response = NSModalResponseOK; bail: //stop w/ response [NSApp stopModalWithCode:response]; //close [self.window close]; return; } //update the UI // set icon, and enable/select 'add' button -(void)updateUI { //icon NSImage* processIcon = nil; //blank // disable 'add' button if(0 == self.path.stringValue.length) { //set state self.addButton.enabled = NO; //bail goto bail; } //get icon processIcon = getIconForProcess(self.path.stringValue); if(nil != processIcon) { //add self.icon.image = processIcon; } //enable 'add' button self.addButton.enabled = YES; bail: return; } //ensure title-bar close also ends the modal - (BOOL)windowShouldClose:(NSWindow *)sender { [NSApp stopModalWithCode:NSModalResponseCancel]; return YES; } @end ================================================ FILE: LuLu/App/AlertWindowController.h ================================================ // // file: AlertWindowController.h // project: lulu (login item) // description: window controller for main firewall alert (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "ParentsWindowController.h" #import "SigningInfoViewController.h" @interface AlertWindowController : NSWindowController /* PROPERTIES */ //alert info @property(nonatomic, retain)NSDictionary* alert; //touch bar @property(nonatomic, retain)NSTouchBar* touchBar; //reply @property (nonatomic, copy)void(^reply)(NSDictionary*); /* TOP */ //process icon @property (weak) IBOutlet NSImageView *processIcon; //process name @property (weak) IBOutlet NSTextField *processName; //general alert message @property (unsafe_unretained) IBOutlet NSTextView *alertMessage; //signing info button @property (weak) IBOutlet NSButton *signingInfoButton; //signing info: popover @property (strong) IBOutlet NSPopover *signingInfoPopover; //virus total: button @property (weak) IBOutlet NSButton *virusTotalButton; //view controller for ancestry view/popover @property (weak) IBOutlet ParentsWindowController *ancestryViewController; //ancestry button @property (weak) IBOutlet NSButton *ancestryButton; //popover for ancestry @property (strong) IBOutlet NSPopover *ancestryPopover; //process ancestry @property (nonatomic, retain)NSMutableArray* processHierarchy; @property (weak) IBOutlet NSButton *allowButton; /* BOTTOM (DETAILS) */ //process id @property (weak) IBOutlet NSTextField *processID; //process args @property (weak) IBOutlet NSTextField *processArgs; //process path @property (unsafe_unretained) IBOutlet NSTextView *processPath; //ip address @property (weak) IBOutlet NSTextField *ipAddress; //port/protocol @property (weak) IBOutlet NSTextField *portProto; //reverse dns name @property (unsafe_unretained) IBOutlet NSTextView *reverseDNS; //ancestry view @property (strong) IBOutlet NSView *ancestryView; //outline view in ancestry popover @property (weak) IBOutlet NSOutlineView *ancestryOutline; //text cell for ancestry popover @property (weak) IBOutlet NSTextFieldCell *ancestryTextCell; //time stamp @property (weak) IBOutlet NSTextField *timeStamp; //action (rule) scope @property (weak) IBOutlet NSPopUpButton *actionScope; //rule durations @property (weak) IBOutlet NSButton *ruleDurationAlways; @property (weak) IBOutlet NSButton *ruleDurationProcess; @property (weak) IBOutlet NSButton *ruleDurationCustom; @property (weak) IBOutlet NSTextField *ruleDurationHours; @property (weak) IBOutlet NSTextField *ruleDurationMinutes; //options view @property (weak) IBOutlet NSView *options; //show options button @property (weak) IBOutlet NSButton* showOptions; //endpoint (e.g. domain) @property(nonatomic, retain)NSString* endpoint; /* METHODS */ //open signing info popover -(void)openSigningInfoPopover; //button handler // ->block/allow, and then close -(IBAction)handleUserResponse:(id)sender; //set wrapping -(void)setWrapping:(NSTextView *)textView; @end ================================================ FILE: LuLu/App/AlertWindowController.m ================================================ // // file: AlertWindowController.m // project: lulu // description: window controller for main firewall alert // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Foundation; #import #import "consts.h" #import "utilities.h" #import "AppDelegate.h" #import "XPCDaemonClient.h" #import "AlertWindowController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; @implementation AlertWindowController @synthesize alert; @synthesize processIcon; @synthesize processName; @synthesize ancestryButton; @synthesize ancestryPopover; @synthesize processHierarchy; @synthesize virusTotalButton; @synthesize signingInfoButton; #define DEFAULT_WINDOW_HEIGHT 244 #define DEFAULT_WINDOW_HEIGHT_EXPANDED 452 //center window // also, transparency -(void)awakeFromNib { //center [self.window center]; //full size content view for translucency self.window.styleMask = self.window.styleMask | NSWindowStyleMaskFullSizeContentView; //title bar translucency self.window.titlebarAppearsTransparent = YES; //move via background self.window.movableByWindowBackground = YES; //set hour formatter NSNumberFormatter *hoursFormatter = [[NSNumberFormatter alloc] init]; hoursFormatter.minimum = @0; hoursFormatter.maximum = @23; hoursFormatter.allowsFloats = NO; self.ruleDurationHours.formatter = hoursFormatter; //set minute formatter NSNumberFormatter *minutesFormatter = [[NSNumberFormatter alloc] init]; minutesFormatter.minimum = @0; minutesFormatter.maximum = @59; minutesFormatter.allowsFloats = NO; self.ruleDurationMinutes.formatter = minutesFormatter; return; } //delegate method // populate/configure alert window -(void)windowDidLoad { //process pid pid_t processID = 0; //process args NSMutableString* arguments = nil; //timestamp formatter NSDateFormatter *timeFormat = nil; //paragraph style NSMutableParagraphStyle* paragraphStyle = nil; //title attributes (for temporary label) NSMutableDictionary* titleAttributes = nil; //preferences NSDictionary* preferences = nil; //height NSUInteger height = 0; //url NSURL* url = nil; //selected duration NSInteger lastRuleDurationTag = 0; //init paragraph style paragraphStyle = [[NSMutableParagraphStyle alloc] init]; //init dictionary for title attributes titleAttributes = [NSMutableDictionary dictionary]; //extract process ID processID = [self.alert[KEY_PROCESS_ID] intValue]; //get preferences preferences = [xpcDaemonClient getPreferences]; //do we have access? // build hierarchy here if(0 == kill(processID, 0)) { //extract self.processHierarchy = generateProcessHierarchy(processID); //dbg msg os_log_debug(logHandle, "(re)generated process hierarchy: %{public}@", self.processHierarchy); } else { //extract self.processHierarchy = alert[KEY_PROCESS_ANCESTORS]; } //disable ancestory button if no ancestors if(0 == self.processHierarchy.count) { //disable self.ancestryButton.enabled = NO; } //disable VirusTotal button if preference is set // this is enough, as all VT integration only occurs when user interacts with button if(YES == [preferences[PREF_NO_VT_MODE] boolValue]) { //dbg msg os_log_debug(logHandle, "user has disabled VirusTotal integrations, so disabling VT button"); //disable self.virusTotalButton.enabled = NO; } //default to using url // though we trim as we don't want the whole URL if(nil != self.alert[KEY_URL]) { //init url url = [NSURL URLWithString:self.alert[KEY_URL]]; //extract host self.endpoint = url.host; } //use IP address else { //set self.endpoint = self.alert[KEY_HOST]; } //sanity check if(nil == self.endpoint) { //set self.endpoint = NSLocalizedString(@"unknown", @"unknown"); } /* TOP */ //set process icon self.processIcon.image = getIconForProcess(self.alert[KEY_PATH]); //process signing info [self setSigningIcon]; //set name self.processName.stringValue = self.alert[KEY_PROCESS_NAME]; //ensure process name clips self.processName.wantsLayer = YES; self.processName.layer.masksToBounds = YES; //alert message self.alertMessage.string = [NSString stringWithFormat:NSLocalizedString(@"is connecting to %@", @"is connecting to %@"), self.endpoint]; //set tooltip to full URL if(nil != url) { //use (full) URLs self.alertMessage.toolTip = [NSString stringWithFormat:NSLocalizedString(@"Full URL: %@", @"Full URL: %@"), url.absoluteString]; } /* BOTTOM (DETAILS) */ //process pid self.processID.stringValue = [self.alert[KEY_PROCESS_ID] stringValue]; //process args // none? means error if(0 == [self.alert[KEY_PROCESS_ARGS] count]) { //unknown self.processArgs.stringValue = NSLocalizedString(@"unknown", @"unknown"); } //process args // only one? means, argv[0] and none else if(1 == [self.alert[KEY_PROCESS_ARGS] count]) { //none self.processArgs.stringValue = NSLocalizedString(@"none", @"none"); } //process args // more than one? create string of all else { //alloc arguments = [NSMutableString string]; //add each // but skip argv[0] for(NSUInteger i=0; i<[self.alert[KEY_PROCESS_ARGS] count]; i++) { //skip first if(0 == i) continue; //add arg [arguments appendFormat:@"%@ ", [self.alert[KEY_PROCESS_ARGS] objectAtIndex:i]]; } //add to UI self.processArgs.stringValue = arguments; } //set tooltip self.processArgs.toolTip = [NSString stringWithFormat:NSLocalizedString(@"Process Arguments: %@", @"Process Arguments: %@"), self.processArgs.stringValue]; //process path for normal processes if(YES != [self.alert[KEY_PROCESS_DELETED] boolValue]) { //add as is self.processPath.string = self.alert[KEY_PATH]; } //deleted processes // strike thru, so user knows else { //strike thru [self.processPath.textStorage setAttributedString: [[NSAttributedString alloc] initWithString:self.alert[KEY_PATH] attributes:@{NSStrikethroughStyleAttributeName:@(NSUnderlineStyleSingle)}]]; } //set wrapping [self setWrapping:self.processPath]; //set tooltip self.processPath.toolTip = [NSString stringWithFormat:NSLocalizedString(@"Process Path: %@", @"Process Path: %@"), self.processPath.string]; //ensure process path clips self.processPath.wantsLayer = YES; self.processPath.layer.masksToBounds = YES; //ip address self.ipAddress.stringValue = (nil != self.alert[KEY_HOST]) ? self.alert[KEY_HOST] : NSLocalizedString(@"unknown", @"unknown"); //port & proto self.portProto.stringValue = [NSString stringWithFormat:@"%@ (%@)", self.alert[KEY_ENDPOINT_PORT], [self convertProtocol:self.alert[KEY_PROTOCOL]]]; //alloc time formatter timeFormat = [[NSDateFormatter alloc] init]; //set format timeFormat.dateFormat = @"HH:mm:ss"; //add timestamp self.timeStamp.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Time stamp: %@", @"Time stamp: %@"), [timeFormat stringFromDate:[[NSDate alloc] init]]]; //set paragraph style to left paragraphStyle.alignment = NSTextAlignmentLeft; //set paragraph attribute for temporary label titleAttributes[NSParagraphStyleAttributeName] = paragraphStyle; //set color to label default titleAttributes[NSForegroundColorAttributeName] = [NSColor labelColor]; //set font titleAttributes[NSFontAttributeName] = [NSFont fontWithName:@"Menlo-Regular" size:12]; //process deleted? // rule scope can only be temporary (as we don't have a path) if(YES == [self.alert[KEY_PROCESS_DELETED] boolValue]) { //on self.ruleDurationProcess.state = NSControlStateValueOn; //disable others self.ruleDurationAlways.enabled = NO; self.ruleDurationCustom.enabled = NO; } //otherwise make sure others are (always) enabled else { //enable self.ruleDurationAlways.enabled = YES; self.ruleDurationCustom.enabled = YES; } //set rule scope // ...based on last one [self.actionScope selectItemAtIndex:[preferences[PREF_ALERT_LAST_RULE_SCOPE] integerValue]]; //grab last rule duration tag lastRuleDurationTag = [preferences[PREF_ALERT_LAST_RULE_DURATION] integerValue]; //not set // default to always if(0 == lastRuleDurationTag) { self.ruleDurationAlways.state = NSControlStateValueOn; } //set rule duration // ...based on last one else if(lastRuleDurationTag == self.ruleDurationAlways.tag) { //set: on self.ruleDurationAlways.state = NSControlStateValueOn; } else if(lastRuleDurationTag == self.ruleDurationProcess.tag) { //set: on self.ruleDurationProcess.state = NSControlStateValueOn; } else if(lastRuleDurationTag == self.ruleDurationCustom.tag) { //set: on self.ruleDurationCustom.state = NSControlStateValueOn; } //show touch bar [self initTouchBar]; //default height height = DEFAULT_WINDOW_HEIGHT; //resize to handle size of alert [self.window setFrame:NSMakeRect(self.window.frame.origin.x, self.window.frame.origin.y, self.window.frame.size.width, height) display:YES]; //make 'allow' button first responder dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ //set first responder [self.window makeFirstResponder:self.allowButton]; }); //show details? // if user expanded them on the last alert if(NSControlStateValueOn == [preferences[PREF_ALERT_SHOW_OPTIONS] integerValue]) { //set 'options' button state to on self.showOptions.state = NSControlStateValueOn; //toggle, to show options [self toggleOptionsView:self.showOptions]; } //otherwise // (re)set back to defaults else { //default min [self.window setContentMinSize:NSMakeSize(self.window.frame.size.width, DEFAULT_WINDOW_HEIGHT)]; //default max [self.window setContentMaxSize:NSMakeSize(self.window.frame.size.width, DEFAULT_WINDOW_HEIGHT)]; //(re)set action scope [self.actionScope selectItemAtIndex:ACTION_SCOPE_PROCESS]; //(re)set rule scope self.ruleDurationAlways.state = NSControlStateValueOn; } bail: return; } //covert number protocol to name -(NSString*)convertProtocol:(NSNumber*)protocol { //protocol NSString* name = nil; //convert switch(protocol.intValue) { //tcp case IPPROTO_TCP: name = @"TCP"; break; //udp case IPPROTO_UDP: name = @"UDP"; break; //?? default: name = [NSString stringWithFormat:NSLocalizedString(@"", @""), [self.alert[KEY_PROTOCOL] intValue]]; } return name; } //set signing icon -(void)setSigningIcon { //signing info NSDictionary* signingInfo = nil; //extract signingInfo = self.alert[KEY_CS_INFO]; //dbg msg os_log_debug(logHandle, "signing info: %{public}@", signingInfo); //none? // just set to unknown if(nil == signingInfo) { //set icon signingInfoButton.image = [NSImage imageNamed:@"SignedUnknown"]; //bail goto bail; } //parse signing info switch([signingInfo[KEY_CS_STATUS] intValue]) { //happily signed case noErr: //item signed by apple if(Apple == [signingInfo[KEY_CS_SIGNER] intValue]) { //set icon signingInfoButton.image = [NSImage imageNamed:@"SignedApple"]; } //signed by dev id/ad hoc, etc else { //set icon signingInfoButton.image = [NSImage imageNamed:@"Signed"]; } break; //unsigned case errSecCSUnsigned: //set icon signingInfoButton.image = [NSImage imageNamed:@"Unsigned"]; break; default: //set icon signingInfoButton.image = [NSImage imageNamed:@"SignedUnknown"]; } bail: return; } //automatically invoked when user clicks signing icon // depending on state, show/populate the popup, or close it -(IBAction)signingInfoButtonHandler:(id)sender { //not open? // show popover if(YES != self.signingInfoPopover.isShown) { //open [self openSigningInfoPopover]; } //otherwise close it else { //close [self.signingInfoPopover close]; } return; } //open signing info popover -(void)openSigningInfoPopover { //view controller SigningInfoViewController* popoverDelegate = nil; //set button state self.signingInfoButton.state = NSControlStateValueOn; //grab delegate popoverDelegate = (SigningInfoViewController*)self.signingInfoPopover.delegate; //set icon image popoverDelegate.icon.image = self.signingInfoButton.image; //set alert info popoverDelegate.alert = self.alert; //show popover [self.signingInfoPopover showRelativeToRect:[self.signingInfoButton bounds] ofView:self.signingInfoButton preferredEdge:NSMaxYEdge]; return; } //VT button handler // open user's browser w/ VT results -(IBAction)vtButtonHandler:(id)sender { NSString* path = nil; NSString* hash = nil; //default path = self.processPath.string; //package? // get path of binary from bundle if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:self.processPath.string]) { //get path path = getBundleExecutable(self.processPath.string); } //hash if(path) { hash = hashFile(path); } if(hash) { //dbg msg os_log_debug(logHandle, "%{public}@ hashed to %{public}@ for VT", path, hash); //open/show in browser [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://www.virustotal.com/gui/file/%@", hash]]]; } //error else { //show error showAlert(NSAlertStyleWarning, [NSString stringWithFormat:NSLocalizedString(@"ERROR: Failed to hash %@", @"ERROR: Failed to hash %@"), self.processName.stringValue], nil, @[NSLocalizedString(@"OK", @"OK")]); } return; } //invoked when user clicks process ancestry button // depending on state, show/populate the popup, or close it -(IBAction)ancestryButtonHandler:(id)sender { //open popover if(NSControlStateValueOn == self.ancestryButton.state) { //add the index value to each process in the hierarchy // used to populate outline/table for(NSUInteger i = 0; iset (some) extra rows if(self.ancestryViewController.processHierarchy.count < 4) { //5 total extraRows = 4 - self.ancestryViewController.processHierarchy.count; } //calc total window height // ->number of rows + extra rows, * height popoverHeight = (self.ancestryViewController.processHierarchy.count + extraRows + 2) * [self.ancestryOutline rowHeight]; //get window's frame popoverFrame = self.ancestryView.frame; //calculate max line width for(NSUInteger i=0; ifirst w/ indentation currentRowWidth = [self.ancestryOutline indentationPerLevel] * (i+1); //calculate width // ->then size of string in row currentRowWidth += [currentRow sizeWithAttributes: @{NSFontAttributeName: self.ancestryTextCell.font}].width; //save it greater than max if(maxRowWidth < currentRowWidth) { //save maxRowWidth = currentRowWidth; } } //add some padding // ->scroll bar, etc maxRowWidth += 50; //set height popoverFrame.size.height = popoverHeight; //set width popoverFrame.size.width = maxRowWidth; //set new frame self.ancestryView.frame = popoverFrame; return; } //close any open popups -(void)closePopups { //process ancestry popup if(NSControlStateValueOn == self.ancestryButton.state) { //close [self.ancestryPopover close]; //set button state to off self.ancestryButton.state = NSControlStateValueOff; } //signing info popup if(NSControlStateValueOn == self.signingInfoButton.state) { //close [self.signingInfoPopover close]; //set button state to off self.signingInfoButton.state = NSControlStateValueOff; } return; } //button handler // close popups, grap user response/send to daemon, save 'preferences' (options shown, etc) -(IBAction)handleUserResponse:(id)sender { //rule expiration NSDate* expiration = nil; //show options state NSInteger showOptionsState = 0; //rule scope index NSInteger ruleScopeIndex = 0; //rule duration NSInteger ruleDurationTag = 0; //response to daemon NSMutableDictionary* alertResponse = nil; //dbg msg os_log_debug(logHandle, "handling user response"); //grab state showOptionsState = self.showOptions.state; //grab action scope index ruleScopeIndex = self.actionScope.indexOfSelectedItem; //get selected rule duration if(self.ruleDurationAlways.state == NSControlStateValueOn) { //save ruleDurationTag = self.ruleDurationAlways.tag; } else if(self.ruleDurationProcess.state == NSControlStateValueOn) { //save ruleDurationTag = self.ruleDurationProcess.tag; } else if(self.ruleDurationCustom.state == NSControlStateValueOn) { //save ruleDurationTag = self.ruleDurationCustom.tag; } //init alert response // start w/ copy of received alert alertResponse = [self.alert mutableCopy]; //set type as user alertResponse[KEY_TYPE] = @(RULE_TYPE_USER); //add current user alertResponse[KEY_USER_ID] = @(getuid()); //add user response alertResponse[KEY_ACTION] = @(((NSButton*)sender).tag); //add action scope alertResponse[KEY_SCOPE] = @(ruleScopeIndex); //rule duration temporary (pid)? if(NSControlStateValueOn == self.ruleDurationProcess.state) { //set flag alertResponse[KEY_DURATION_PROCESS] = @1; } //rule duration temporary (expiration)? else if(NSControlStateValueOn == self.ruleDurationCustom.state) { NSInteger hours = self.ruleDurationHours.integerValue; NSInteger minutes = self.ruleDurationMinutes.integerValue; NSTimeInterval totalSeconds = (hours * 3600) + (minutes * 60); //sanity check if(totalSeconds <= 0) { //lower window level (so alert can show above) [self.window setLevel:NSNormalWindowLevel]; //show error showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: Enter a non-zero duration", @"ERROR: Enter a non-zero duration"), nil, @[NSLocalizedString(@"OK", @"OK")]); //(re)set window level [self.window setLevel:NSPopUpMenuWindowLevel]; //bail here return; } //covert and add else { //convert to data expiration = [NSDate dateWithTimeIntervalSinceNow:totalSeconds]; //dbg msg os_log_debug(logHandle, "rule expiration: %{public}@", expiration); //add alertResponse[KEY_DURATION_EXPIRATION] = expiration; } } //set endpoint addr alertResponse[KEY_ENDPOINT_ADDR] = self.endpoint; //close popups [self closePopups]; //close window [self.window close]; //dbg msg os_log_debug(logHandle, "replying to alert %{public}@", alertResponse); //reply self.reply(alertResponse); //save preferences // includes options shown/last action scope, etc. [xpcDaemonClient updatePreferences:@{PREF_ALERT_SHOW_OPTIONS: @(showOptionsState), PREF_ALERT_LAST_RULE_SCOPE: @(ruleScopeIndex), PREF_ALERT_LAST_RULE_DURATION: @(ruleDurationTag)}]; //set app's background/foreground state [((AppDelegate*)[[NSApplication sharedApplication] delegate]) setActivationPolicy]; return; } //init/show touch bar -(void)initTouchBar { //touch bar items NSArray *touchBarItems = nil; //alloc/init self.touchBar = [[NSTouchBar alloc] init]; if(nil == self.touchBar) { //no touch bar? goto bail; } //set delegate self.touchBar.delegate = self; //set id self.touchBar.customizationIdentifier = @BUNDLE_ID; //init items touchBarItems = @[@".icon", @".label", @".block", @".allow"]; //set items self.touchBar.defaultItemIdentifiers = touchBarItems; //set customization items self.touchBar.customizationAllowedItemIdentifiers = touchBarItems; bail: return; } //delegate method // init item for touch bar -(NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { //icon view NSImageView *iconView = nil; //icon NSImage* icon = nil; //item NSCustomTouchBarItem *touchBarItem = nil; //init item touchBarItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; //icon if(YES == [identifier isEqualToString: @".icon" ]) { //init icon view iconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 30.0, 30.0)]; //enable layer [iconView setWantsLayer:YES]; //set color [iconView.layer setBackgroundColor:[[NSColor windowBackgroundColor] CGColor]]; //mask iconView.layer.masksToBounds = YES; //round corners iconView.layer.cornerRadius = 3.0; //load icon image icon = [NSImage imageNamed:@"Icon"]; //set size icon.size = CGSizeMake(30, 30); //add image iconView.image = icon; //set view touchBarItem.view = iconView; } //label else if(YES == [identifier isEqualToString:@".label"]) { //item label touchBarItem.view = [NSTextField labelWithString:[NSString stringWithFormat:@"%@ %@", self.processName.stringValue,self.alertMessage.string]]; } //block button else if(YES == [identifier isEqualToString:@".block"]) { //init button touchBarItem.view = [NSButton buttonWithTitle: @"Block" target:self action: @selector(handleUserResponse:)]; //set tag // 0: block ((NSButton*)touchBarItem.view).tag = 0; } //allow button else if(YES == [identifier isEqualToString:@".allow"]) { //init button touchBarItem.view = [NSButton buttonWithTitle: @"Allow" target:self action: @selector(handleUserResponse:)]; //set tag // 1: allow ((NSButton*)touchBarItem.view).tag = 1; } return touchBarItem; } //groups radio buttons -(IBAction)ruleDurationHandler:(id)sender { //need this method so radio buttons are mutually exclusive } //show / hide option view -(IBAction)toggleOptionsView:(id)sender { //frame NSRect origin = {0}; //dbg msg os_log_debug(logHandle, "toggling option's view, state: %ld", (long)self.showOptions.state); //frame NSRect windowFrame = self.window.frame; //on if(NSControlStateValueOn == self.showOptions.state) { origin = self.window.contentView.frame; origin.origin.y = self.window.contentView.frame.size.height; windowFrame.size.height += NSHeight(self.options.frame); //NSHeight(self.options.frame); windowFrame.origin.y -= NSHeight(self.options.frame); [self.window setContentMinSize:NSMakeSize(self.window.frame.size.width, DEFAULT_WINDOW_HEIGHT_EXPANDED)]; [self.window setContentMaxSize:NSMakeSize(self.window.frame.size.width, DEFAULT_WINDOW_HEIGHT_EXPANDED)]; [self.options setHidden:NO]; [self.window layoutIfNeeded]; [self.window displayIfNeeded]; //dbg msg os_log_debug(logHandle, "added option view to window"); } //off else { //[self.options removeFromSuperview]; [self.options setHidden:YES]; //dbg msg os_log_debug(logHandle, "removed option view from window"); windowFrame.size.height -= NSHeight(self.options.frame); windowFrame.origin.y += NSHeight(self.options.frame); [self.window setContentMinSize:NSMakeSize(self.window.frame.size.width, DEFAULT_WINDOW_HEIGHT)]; [self.window setContentMaxSize:NSMakeSize(self.window.frame.size.width, DEFAULT_WINDOW_HEIGHT)]; } //resize [self.window setFrame:windowFrame display:YES animate:YES]; return; } //set wrapping -(void)setWrapping:(NSTextView*)textView { NSRange fullRange = {0,0}; NSMutableAttributedString* mutableAttrString = nil; mutableAttrString = [textView.textStorage mutableCopy]; fullRange = NSMakeRange(0, mutableAttrString.length); [mutableAttrString enumerateAttributesInRange:fullRange options:0 usingBlock:^(NSDictionary* attrs, NSRange range, BOOL *stop) { NSMutableParagraphStyle* mutableParagraphStyle = nil; NSParagraphStyle* existingStyle = attrs[NSParagraphStyleAttributeName]; if(nil != existingStyle) { mutableParagraphStyle = [existingStyle mutableCopy]; } else { mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init]; } mutableParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping; NSMutableDictionary* newAttrs = [attrs mutableCopy]; newAttrs[NSParagraphStyleAttributeName] = mutableParagraphStyle; [mutableAttrString setAttributes:newAttrs range:range]; }]; [textView.textStorage setAttributedString:mutableAttrString]; return; } @end ================================================ FILE: LuLu/App/App.entitlements ================================================ com.apple.developer.networking.networkextension content-filter-provider-systemextension com.apple.developer.system-extension.install com.apple.security.application-groups $(TeamIdentifierPrefix)com.objective-see.lulu ================================================ FILE: LuLu/App/AppDelegate.h ================================================ // // AppDelegate.h // LuLu // // Created by Patrick Wardle on 8/1/20. // Copyright (c) 2020 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; @import NetworkExtension; @import SystemExtensions; #import "StatusBarItem.h" #import "XPCDaemonClient.h" #import "AboutWindowController.h" #import "PrefsWindowController.h" #import "RulesWindowController.h" #import "UpdateWindowController.h" #import "StartupWindowController.h" #import "WelcomeWindowController.h" @interface AppDelegate : NSObject /* PROPERTIES */ //configure window controller @property(nonatomic, retain)StartupWindowController* startupWindowController; //uninstall (deactivation) request @property(nonatomic, retain)OSSystemExtensionRequest *uninstallRequest; //welcome view controller @property(nonatomic, retain)WelcomeWindowController* welcomeWindowController; //status bar menu/sub-menus @property(strong) IBOutlet NSMenu* statusMenu; //profile menu item @property(strong) IBOutlet NSMenuItem *profilesMenuItem; //switch profile menu item @property (weak) IBOutlet NSMenuItem *profileSwitchMenuItem; //list of profiles mene @property (weak) IBOutlet NSMenu *profilesMenu; //status bar menu controller @property(nonatomic, retain)StatusBarItem* statusBarItemController; //about window controller @property(nonatomic, retain)AboutWindowController* aboutWindowController; //preferences window controller @property(nonatomic, retain)PrefsWindowController* prefsWindowController; //rules window controller @property(nonatomic, retain)RulesWindowController* rulesWindowController; //update window controller @property(nonatomic, retain)UpdateWindowController* updateWindowController; /* METHODS */ //first launch? // check for install time(stamp) -(BOOL)isFirstTime; //finish up initializations -(void)completeInitialization:(NSDictionary*)initialPreferenes; //set app foreground/background // determined by the app's window count -(void)setActivationPolicy; //'rules' menu item handler // alloc and show rules window -(IBAction)showRules:(id)sender; //'preferences' menu item handler // alloc and show preferences window -(void)showPreferences:(NSString*)itemID; //preferences changed // for now, just check status bar icon setting -(void)preferencesChanged:(NSDictionary*)preferences; //profiles changed -(void)profilesChanged; //toggle (status) bar icon -(void)toggleIcon:(NSDictionary*)preferences; //quit -(IBAction)quit:(id)sender; //uninstall -(IBAction)uninstall:(id)sender; @end ================================================ FILE: LuLu/App/AppDelegate.m ================================================ // // AppDelegate.m // LuLu // // Created by Patrick Wardle on 8/1/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "Update.h" #import "Configure.h" #import "Extension.h" #import "AppDelegate.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //alert windows NSMutableDictionary* alerts = nil; //xpc for daemon comms XPCDaemonClient* xpcDaemonClient = nil; @interface AppDelegate () @property (weak) IBOutlet NSWindow *window; @end @implementation AppDelegate @synthesize aboutWindowController; @synthesize prefsWindowController; @synthesize rulesWindowController; @synthesize updateWindowController; @synthesize startupWindowController; @synthesize statusBarItemController; @synthesize welcomeWindowController; //main app interface -(void)applicationDidFinishLaunching:(NSNotification *)aNotification { //delay NSInteger delay = 1; //current version NSOperatingSystemVersion version = {0}; //alert response NSModalResponse response = 0; //dbg msg os_log_debug(logHandle, "%s", __PRETTY_FUNCTION__); //don't relaunch [NSApp disableRelaunchOnLogin]; //v1.0 version installed? // prompt / provide link to uninstall instructions / exit if(YES == [NSFileManager.defaultManager fileExistsAtPath:[INSTALL_DIRECTORY stringByAppendingPathComponent:@"LuLu.bundle"]]) { //show alert response = showAlert(NSAlertStyleWarning, NSLocalizedString(@"Old Version of LuLu Installed", @"Old Version of LuLu Installed"), NSLocalizedString(@"This must be uninstalled before continuing. Click 'More Info' to learn how to uninstall it", @"This must be uninstalled before continuing\r\n.Click 'More Info' to learn how to uninstall it."), @[NSLocalizedString(@"More Info", @"More Info"), NSLocalizedString(@"Cancel", @"Cancel")]); //open link to uninstall instructions if(NSAlertSecondButtonReturn == response) { //open [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@#uninstall_v1", PRODUCT_URL]]]; } //(always) exit [NSApplication.sharedApplication terminate:self]; } //Apple: item's w/ System Extensions must be run from /Applications 🤷🏻‍♂️ if(![NSBundle.mainBundle.bundlePath hasPrefix:@"/Applications/"]) { //dbg msg os_log_debug(logHandle, "LuLu running from %{public}@, not from within /Applications", NSBundle.mainBundle.bundlePath); //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"LuLu must run from within /Applications\r\n", @"LuLu must run from within /Applications\r\n"), NSLocalizedString(@"...please copy it into /Applications and re-launch.", @"...please copy it into /Applications and re-launch."), @[NSLocalizedString(@"OK",@"OK")]); //exit [NSApplication.sharedApplication terminate:self]; } //first time? // show/walk thru welcome screen(s) // ...will call back here to complete initializations if(YES == [self isFirstTime]) { //dbg msg os_log_debug(logHandle, "first launch, will kick off welcome window(s)"); //alloc window controller welcomeWindowController = [[WelcomeWindowController alloc] initWithWindowNibName:@"Welcome"]; //set activation policy [self setActivationPolicy]; //show window [self.welcomeWindowController showWindow:self]; //make front [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; //make window front [self.startupWindowController.window makeKeyAndOrderFront:nil]; //install (self as) login item if(YES != toggleLoginItem(NSBundle.mainBundle.bundleURL, ACTION_INSTALL_FLAG)) { //err msg os_log_error(logHandle, "ERROR: failed to install self as login item"); } //dbg msg else { os_log_debug(logHandle, "installed self as login item"); } } //subsequent launches... // launch extension & complete initializations else { //dbg os_log_debug(logHandle, "subsequent launch..."); //started by user? // show startup msg.... if(YES == launchedByUser()) { //dbg msg os_log_debug(logHandle, "showing startup window..."); //alloc/init startupWindowController = [[StartupWindowController alloc] initWithWindowNibName:@"StartupWindowController"]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //make window front [self.startupWindowController.window makeKeyAndOrderFront:nil]; //make it modal(ish) [self.startupWindowController.window setLevel:NSPopUpMenuWindowLevel]; //show window [self.startupWindowController showWindow:nil]; } //grab OS version version = NSProcessInfo.processInfo.operatingSystemVersion; //macOS 15 // but less than 15.3? ...increase delay so users can see warning if(version.majorVersion == 15 && version.minorVersion < 3) { //3 seconds delay = 3; } //wait a few seconds, so that startup window can show... then fade out dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ //fade out fadeOut(self.startupWindowController.window, 1.0f); //(re)activate extension // this will call back to complete inits when done dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //extension Extension* extension = nil; //wait semaphore dispatch_semaphore_t semaphore = 0; //init extension object extension = [[Extension alloc] init]; //init wait semaphore semaphore = dispatch_semaphore_create(0); //kick off extension activation request [extension toggleExtension:ACTION_ACTIVATE reply:^(NSError* error) { //dbg msg os_log_debug(logHandle, "extension 'activate' returned"); //error if(error) { //err msg os_log_error(logHandle, "ERROR: failed to activate extension"); //show error on main thread dispatch_async(dispatch_get_main_queue(), ^{ //show alert showAlert(NSAlertStyleCritical, NSLocalizedString(@"ERROR: activation failed", @"ERROR: activation failed"), NSLocalizedString(@"failed to activate system/network extension", @"failed to activate system/network extension"), @[NSLocalizedString(@"OK", @"OK")]); //exit [NSApplication.sharedApplication terminate:self]; }); return; } //dbg msg os_log_debug(logHandle, "activated system extension, will now activate network filter..."); //(always) activate network filter // side affect is that it launches system extension if(YES != [[[Extension alloc] init] toggleNetworkExtension:ACTION_ACTIVATE]) { //err msg os_log_error(logHandle, "ERROR: failed to activate network filter"); //show alert/exit on main thread dispatch_async(dispatch_get_main_queue(), ^{ //show alert showAlert(NSAlertStyleCritical, NSLocalizedString(@"ERROR: activation failed", @"ERROR: activation failed"), NSLocalizedString(@"failed to activate network filter",@"failed to activate network filter"), @[NSLocalizedString(@"OK", @"OK")]); //bye [NSApplication.sharedApplication terminate:self]; }); } //dbg msg os_log_debug(logHandle, "network filter activated/enabled"); //wait to ensure extension is up an running do { //dbg msg os_log_debug(logHandle, "waiting for %{public}@", EXT_BUNDLE_ID); //nap [NSThread sleepForTimeInterval:0.25f]; } while(YES != [extension isExtensionRunning]); //dbg msg os_log_debug(logHandle, "%{public}@ is off and running", EXT_BUNDLE_ID); //complete initialization on main thread dispatch_async(dispatch_get_main_queue(), ^{ //complete initializations [self completeInitialization:nil]; }); //signal semaphore dispatch_semaphore_signal(semaphore); }]; //dbg msg os_log_debug(logHandle, "waiting system extension & network filter activation..."); //wait for extension semaphore dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }); }); } bail: return; } //first launch? // check for install time(stamp) -(BOOL)isFirstTime { return (nil == [[NSMutableDictionary dictionaryWithContentsOfFile:[INSTALL_DIRECTORY stringByAppendingPathComponent:PREFS_FILE]] objectForKey:PREF_INSTALL_TIMESTAMP]); } //handle user double-clicks // app is (likely) already running as login item, so show (or) activate window -(BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)hasVisibleWindows { //extension Extension* extension = nil; //init extension object extension = [[Extension alloc] init]; //dbg msg os_log_debug(logHandle, "method '%s' invoked (hasVisibleWindows: %d)", __PRETTY_FUNCTION__, hasVisibleWindows); //extention isn't running? // show alert, otherwise things get confusing if(YES != [extension isExtensionRunning]) { //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"LuLu's Network Extension Is Not Running", @"LuLu's Network Extension Is Not Running"), NSLocalizedString(@"Extensions must be manually approved via System Settings (General > Login Items & Extensions > Network Extensions).",@"Extensions must be manually approved via System Settings (General > Login Items & Extensions > Network Extensions)."), @[NSLocalizedString(@"OK", @"OK")]); //bail goto bail; } //no visible window(s) // default to show preferences if(YES != hasVisibleWindows) { //show prefs [self showPreferences:nil]; } bail: return NO; } //'rules' menu item handler // alloc and show rules window -(IBAction)showRules:(id)sender { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //alloc rules window controller if(nil == self.rulesWindowController) { //alloc rulesWindowController = [[RulesWindowController alloc] initWithWindowNibName:@"Rules"]; } //configure (UI) [self.rulesWindowController configure]; //make active [self makeActive:self.rulesWindowController]; return; } //'preferences' menu item handler // alloc and show preferences window -(void)showPreferences:(NSString*)itemID { //dbg msg os_log_debug(logHandle, "method '%s' invoked with %@", __PRETTY_FUNCTION__, itemID); //alloc prefs window controller if(nil == self.prefsWindowController) { //alloc prefsWindowController = [[PrefsWindowController alloc] initWithWindowNibName:@"Preferences"]; } //make active [self makeActive:self.prefsWindowController]; //select tab [self.prefsWindowController switchTo:itemID]; return; } //'about' menu item handler // alloc/show the about window -(IBAction)showAbout:(id)sender { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //alloc/init settings window if(nil == self.aboutWindowController) { //alloc/init aboutWindowController = [[AboutWindowController alloc] initWithWindowNibName:@"AboutWindow"]; } //center window [self.aboutWindowController.window center]; //show window [self.aboutWindowController showWindow:self]; return; } //preferences changed // for now, just check status bar icon setting -(void)preferencesChanged:(NSDictionary*)preferences { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //trigger rules reload, because some prefs influence how rules are displayed [[NSNotificationCenter defaultCenter] postNotificationName:RULES_CHANGED object:nil userInfo:nil]; //update status bar [self toggleIcon:preferences]; return; } //profiles changed // update preferences window and status bar menu -(void)profilesChanged { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //tell preferences window [self.prefsWindowController reload]; //tell status menu [self.statusBarItemController setProfile]; return; } //close window handler // close rules || pref window -(IBAction)closeWindow:(id)sender { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //key window NSWindow *keyWindow = nil; //get key window keyWindow = [[NSApplication sharedApplication] keyWindow]; //close // but only for rules/pref/about window if( (keyWindow != self.aboutWindowController.window) && (keyWindow != self.prefsWindowController.window) && (keyWindow != self.rulesWindowController.window) ) { //dbg msg os_log_debug(logHandle, "key window is not rules or pref window, so ignoring..."); //ignore goto bail; } //close [keyWindow close]; //set activation policy [self setActivationPolicy]; bail: return; } //make a window control/window front/active -(void)makeActive:(NSWindowController*)windowController { //make foreground [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; //center [windowController.window center]; //make it key window [windowController.window makeKeyAndOrderFront:self]; //front [windowController.window orderFrontRegardless]; //show it [windowController showWindow:self]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //activate ...more! [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; return; } //toggle (status) bar icon -(void)toggleIcon:(NSDictionary*)preferences { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //should run with icon? // init and show status bar item if(YES != [preferences[PREF_NO_ICON_MODE] boolValue]) { //already showing? if(nil != self.statusBarItemController) { //bail goto bail; } //dbg msg os_log_debug(logHandle, "initializing status bar item/menu"); //alloc/load status bar icon/menu statusBarItemController = [[StatusBarItem alloc] init:self.statusMenu preferences:(NSDictionary*)preferences]; } //run without icon // remove status bar item else { //dbg msg os_log_debug(logHandle, "removing status bar item/menu"); //already removed? if(nil == self.statusBarItemController) { //bail goto bail; } //remove status item [self.statusBarItemController removeStatusItem]; //unset self.statusBarItemController = nil; } bail: return; } //set app foreground/background -(void)setActivationPolicy { //visible window BOOL visibleWindow = NO; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //find any visible windows for(NSWindow* window in NSApp.windows) { //visible window? // that's not status bar? if( (YES == window.isVisible) && (YES != [window.className isEqualToString:@"_NSPopoverWindow"]) && (YES != [window.className isEqualToString:@"NSStatusBarWindow"]) ) { //set flag visibleWindow = YES; //done break; } } //any windows? // bring app to foreground if(YES == visibleWindow) { //dbg msg os_log_debug(logHandle, "window(s) visible, setting policy: NSApplicationActivationPolicyRegular"); //foreground [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; } //no more windows // send app to background else { //dbg msg os_log_debug(logHandle, "window(s) not visible, setting policy: NSApplicationActivationPolicyAccessory"); //background [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; } return; } //finish up initializations // includes enabling network ext if user hasn't disabled -(void)completeInitialization:(NSDictionary*)initialPreferences { //preferences NSDictionary* preferences = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //alloc array for alert (windows) alerts = [NSMutableDictionary dictionary]; //init extension comms // establishes connection to extension xpcDaemonClient = [[XPCDaemonClient alloc] init]; //initial prefs? // send to extension if(nil != initialPreferences) { //set prefs [xpcDaemonClient updatePreferences:initialPreferences]; } //always (reset) disabled // always want enabled on (restart) preferences = [xpcDaemonClient updatePreferences:@{PREF_IS_DISABLED:@NO}]; //dbg msg os_log_debug(logHandle, "loaded preferences %{public}@", preferences); //run with status bar icon? if(YES != [preferences[PREF_NO_ICON_MODE] boolValue]) { //alloc/load nib statusBarItemController = [[StatusBarItem alloc] init:self.statusMenu preferences:(NSDictionary*)preferences]; //dbg msg os_log_debug(logHandle, "initialized/loaded status bar (icon/menu)"); } else { //dbg msg os_log_debug(logHandle, "running in 'no icon' mode (so no need for status bar)"); } //cleanup any expired/temp rules [xpcDaemonClient cleanupRules:NO]; //automatically check for updates? // skipped if launched by user (e.g. first time run) if( (YES != launchedByUser()) && (YES != [preferences[PREF_NO_UPDATE_MODE] boolValue]) ) { //after a 30 seconds // check for updates in background dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { //dbg msg os_log_debug(logHandle, "checking for update..."); //check [self check4Update]; }); } return; } //check for update -(void)check4Update { //update obj Update* update = nil; //init update obj update = [[Update alloc] init]; //check for update // 'updateResponse newVersion:' method will be called when check is done [update checkForUpdate:^(NSUInteger result, NSString* newVersion) { //handle response // new version, show popup switch(result) { //error case Update_Error: os_log_error(logHandle, "ERROR: update check failed"); break; //no updates case Update_None: os_log_debug(logHandle, "no updates available"); break; //this version of macOS, not supported case Update_NotSupported: //dbg msg os_log_debug(logHandle, "update available, but not for this version of macOS"); break; //new version // show update window case Update_Available: //dbg msg os_log_debug(logHandle, "a new version (%@) is available", newVersion); //alloc update window self.updateWindowController = [[UpdateWindowController alloc] initWithWindowNibName:@"UpdateWindow"]; //configure [self.updateWindowController configure:[NSString stringWithFormat:NSLocalizedString(@"a new version (%@) is available!",@"a new version (%@) is available!"), newVersion]]; //center window [self.updateWindowController.window center]; //show it [self.updateWindowController showWindow:self]; //invoke function in background that will make window modal dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //make modal makeModal(self.updateWindowController); }); break; } }]; return; } //quit button handler // do any cleanup, then exit -(IBAction)quit:(id)sender { //response NSModalResponse response = 0; //dbg msg os_log_debug(logHandle, "function '%s' invoked", __PRETTY_FUNCTION__); //show alert response = showAlert(NSAlertStyleInformational, NSLocalizedString(@"Quit LuLu?", @"Quit LuLu?"), NSLocalizedString(@"...this will terminate LuLu, until the next time you log in.", @"...this will terminate LuLu, until the next time you log in."), @[NSLocalizedString(@"Quit", @"Quit"), NSLocalizedString(@"Cancel", @"Cancel")]); //show alert // cancel? ignore if(NSAlertSecondButtonReturn == response) { //dbg msg os_log_debug(logHandle, "user canceled quitting"); //(re)background [self setActivationPolicy]; } //ok // user wants to quit! else { //dbg msg os_log_debug(logHandle, "user confirmed quit"); //slight delay to let alert dismiss dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //config obj Configure* configure = [[Configure alloc] init]; //quit [configure quit]; //and terminate [NSApplication.sharedApplication terminate:self]; }); } return; } //uninstall menu handler // cleanup all the thingz! -(IBAction)uninstall:(id)sender { //response NSModalResponse response = 0; //config obj Configure* configure = nil; //show alert response = showAlert(NSAlertStyleInformational, NSLocalizedString(@"Uninstall LuLu?", @"Uninstall LuLu?"), NSLocalizedString(@"...this will fully remove LuLu from your Mac", @"...this will fully remove LuLu from your Mac"), @[NSLocalizedString(@"Uninstall", @"Uninstall"), NSLocalizedString(@"Cancel", @"Cancel")]); //cancel? ignore if(NSAlertSecondButtonReturn == response) { //dbg msg os_log_debug(logHandle, "user canceled uninstalling"); //(re)background [self setActivationPolicy]; } //ok // user wants to uninstall! else { //dbg msg os_log_debug(logHandle, "user confirmed uninstall"); //init configure = [[Configure alloc] init]; //quit if(YES != [configure uninstall]) { //err msg os_log_error(logHandle, "ERROR: uninstall failed"); } //and terminate [NSApplication.sharedApplication terminate:self]; } bail: return; } @end ================================================ FILE: LuLu/App/Assets.xcassets/Ancestry.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "parents any.pdf" }, { "idiom" : "mac", "filename" : "parents light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "parents dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: LuLu/App/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsFleet.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "darkMode.png" }, { "idiom" : "mac", "filename" : "lightMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "darkMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsHuntress.imageset/Contents.json ================================================ { "images" : [ { "filename" : "darkMode.png", "idiom" : "mac" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ], "filename" : "lightMode.png", "idiom" : "mac" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "darkMode.png", "idiom" : "mac" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsJamf.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "darkMode.png" }, { "idiom" : "mac", "filename" : "lightMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "darkMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsKandji.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "darkMode.png" }, { "idiom" : "mac", "filename" : "lightMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "darkMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsMacPaw.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "darkMode.png" }, { "idiom" : "mac", "filename" : "lightMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "darkMode.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsMalwarebytes.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "logo.png" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsPANW.imageset/Contents.json ================================================ { "images" : [ { "filename" : "darkMode.png", "idiom" : "mac" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ], "filename" : "lightMode.png", "idiom" : "mac" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "darkMode.png", "idiom" : "mac" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsSophos.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "sophos.png" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/FriendsiVerify.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "iVerify.png" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/Heart.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "heart_light-1.pdf" }, { "idiom" : "mac", "filename" : "heart_light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "heart_dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/Icon.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "LoginItem Any.pdf" }, { "idiom" : "mac", "filename" : "LoginItem Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "LoginItem Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/InstallAllow.imageset/Contents.json ================================================ { "images" : [ { "filename" : "allow.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" }, { "filename" : "allow_es.png", "idiom" : "universal", "locale" : "es", "scale" : "1x" }, { "idiom" : "universal", "locale" : "es", "scale" : "2x" }, { "idiom" : "universal", "locale" : "es", "scale" : "3x" }, { "filename" : "allow_ko.png", "idiom" : "universal", "locale" : "ko", "scale" : "1x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "2x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "localizable" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/InstallApprove.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "approve.png", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" }, { "locale" : "es", "idiom" : "universal", "filename" : "approve_es.png", "scale" : "1x" }, { "idiom" : "universal", "locale" : "es", "scale" : "2x" }, { "idiom" : "universal", "locale" : "es", "scale" : "3x" }, { "filename" : "approve_ko.png", "idiom" : "universal", "locale" : "ko", "scale" : "1x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "2x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "localizable" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/InstallApprove_OLD.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "approve.png", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" }, { "locale" : "es", "idiom" : "universal", "filename" : "approve_es.png", "scale" : "1x" }, { "idiom" : "universal", "locale" : "es", "scale" : "2x" }, { "idiom" : "universal", "locale" : "es", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "localizable" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/InstallAuth.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "auth.png", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" }, { "locale" : "es", "idiom" : "universal", "filename" : "auth_es.png", "scale" : "1x" }, { "idiom" : "universal", "locale" : "es", "scale" : "2x" }, { "idiom" : "universal", "locale" : "es", "scale" : "3x" }, { "filename" : "auth_ko.png", "idiom" : "universal", "locale" : "ko", "scale" : "1x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "2x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "localizable" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/InstallBlocked.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "blocked.png", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" }, { "locale" : "es", "idiom" : "universal", "filename" : "blocked_es.png", "scale" : "1x" }, { "idiom" : "universal", "locale" : "es", "scale" : "2x" }, { "idiom" : "universal", "locale" : "es", "scale" : "3x" }, { "filename" : "blocked_ko.png", "idiom" : "universal", "locale" : "ko", "scale" : "1x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "2x" }, { "idiom" : "universal", "locale" : "ko", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "localizable" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/LuLuText.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "lulu_text.pdf" }, { "idiom" : "mac", "filename" : "lulu_text-1.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "lulu_text-2.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/PrefsList.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "icon.png" }, { "idiom" : "mac", "filename" : "icon.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "icon.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/PrefsMode.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "prefsMode Any.pdf" }, { "idiom" : "mac", "filename" : "prefsMode Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "prefsMode Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/PrefsProfiles.imageset/Contents.json ================================================ { "images" : [ { "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ], "filename" : "profiles.png", "idiom" : "universal", "scale" : "1x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "profilesDark.png", "idiom" : "universal", "scale" : "1x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ], "filename" : "profiles@2x.png", "idiom" : "universal", "scale" : "2x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "profilesDark@2x.png", "idiom" : "universal", "scale" : "2x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ], "idiom" : "universal", "scale" : "3x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: LuLu/App/Assets.xcassets/PrefsRules.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "prefsRules Any.pdf" }, { "idiom" : "mac", "filename" : "prefsRules Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "prefsRules Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/PrefsUpdate.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "prefsUpdate Any.pdf" }, { "idiom" : "mac", "filename" : "prefsUpdate Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "prefsUpdate Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/RulesAllow.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "rulesAllow Any.pdf" }, { "idiom" : "mac", "filename" : "rulesAllow Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "rulesAllow Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/RulesBlock.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "block Any.pdf" }, { "idiom" : "mac", "filename" : "block Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "block Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/RulesSystem.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "rulesSystem light.pdf" }, { "idiom" : "mac", "filename" : "rulesSystem light-1.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "rulesSystem dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/Signed.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "signed Any.pdf" }, { "idiom" : "mac", "filename" : "signed Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "signed Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/SignedApple.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "signedApple Any.pdf" }, { "idiom" : "mac", "filename" : "signedApple Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "signedApple Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/SignedUnknown.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "Unknown any.pdf" }, { "idiom" : "mac", "filename" : "Unknown light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "Unknown dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/StatusActive.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "LoginItem StatusBar Active.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/StatusInactive.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "LoginItem StatusBar Inactive.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/Unsigned.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "unsigned Any.pdf" }, { "idiom" : "mac", "filename" : "unsigned Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "unsigned Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/VTIcon.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "VT Light.pdf" }, { "idiom" : "mac", "filename" : "VT Any-1.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "VT Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template", "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/allow.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "rulesAllow Any.pdf" }, { "idiom" : "mac", "filename" : "rulesAllow Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "rulesAllow Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/block.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "block Any.pdf" }, { "idiom" : "mac", "filename" : "block Light.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "block Dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/refresh.imageset/Contents.json ================================================ { "images" : [ { "filename" : "refresh.png", "idiom" : "mac" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ], "filename" : "refresh.png", "idiom" : "mac" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "refresh.png", "idiom" : "mac" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/rulesRecent.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "rulesRecent.png" }, { "idiom" : "mac", "filename" : "rulesRecent.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "rulesRecent.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Assets.xcassets/rulesUnknown.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "rulesUnknown.png" }, { "idiom" : "mac", "filename" : "rulesUnknown.png", "appearances" : [ { "appearance" : "luminosity", "value" : "light" } ] }, { "idiom" : "mac", "filename" : "rulesUnknown.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ] } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: LuLu/App/Base.lproj/AboutWindow.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/AddRule.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/AlertWindow.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/ItemPaths.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/Preferences.xib ================================================ Note: When a new connection is made, sometimes only the IP address may be available. In such cases, domains on the allow or block list cannot be matched, which may impact whether a request is blocked or allowed. Note: A profile defines a set of rules and settings. Once activated, its settings apply LuLu-wide (and can be modified via LuLu's Settings panes). Any new rules will be added only to that profile's ruleset. First create a name for your profile. Then click "Next" to configure the settings for your new profile. ================================================ FILE: LuLu/App/Base.lproj/Rules.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/StartupWindowController.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/StatusBarPopover.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/UpdateWindow.xib ================================================ ================================================ FILE: LuLu/App/Base.lproj/Welcome.xib ================================================ It monitors network activity and alerts you when a program first tries to initiate an outgoing connection, allowing you to permit or deny it. ================================================ FILE: LuLu/App/Configure.h ================================================ // // Configure.h // LuLu // // Created by Patrick Wardle on 2/6/24. // Copyright © 2024 Objective-See. All rights reserved. // #ifndef Configure_h #define Configure_h @import Foundation; @interface Configure : NSObject //quit -(void)quit; //install -(BOOL)install; //upgrade -(BOOL)upgrade; //uninstall -(BOOL)uninstall; @end #endif /* Configure_h */ ================================================ FILE: LuLu/App/Configure.m ================================================ // // Configure.m // LuLu // // Created by Patrick Wardle on 2/6/24. // Copyright © 2024 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "Configure.h" #import "Extension.h" #import "XPCDaemonClient.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; @implementation Configure //init -(id)init { //init if(self = [super init]) { //if needed, in extension comms if(nil == xpcDaemonClient) { //init xpcDaemonClient = [[XPCDaemonClient alloc] init]; } } return self; } //install -(BOOL)install { //flag BOOL installed = NO; //error NSError* error = nil; //source NSString* source = nil; //destination NSString* destination = nil; //dbg msg os_log_debug(logHandle, "function '%s' invoked", __PRETTY_FUNCTION__); //quit LuLu [self quit]; //init source source = NSBundle.mainBundle.bundlePath; //init destination destination = [@"/Applications" stringByAppendingPathComponent:APP_NAME]; //remove any existing LuLu.app if(YES == [NSFileManager.defaultManager fileExistsAtPath:destination]) { //not us? // remove if(YES != [source isEqualToString:destination]) { //remove if(YES != [NSFileManager.defaultManager removeItemAtPath:destination error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to remove %{public}@ (error: %{public}@)", destination, error); goto bail; } } } //copy self into /Applications if(YES != [NSFileManager.defaultManager copyItemAtPath:source toPath:destination error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to move %{public}@ to %{public}@ (error: %{public}@)", source, destination, error); goto bail; } //dbg msg os_log_debug(logHandle, "moved self into /Applications and will launch..."); //now launch if(nil == [NSWorkspace.sharedWorkspace launchApplicationAtURL:[NSURL fileURLWithPath:destination] options:0 configuration:@{} error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to launch %{public}@, (error: %{public}@)", destination, error); goto bail; } //happy installed = YES; bail: return installed; } //upgrade // same as install -(BOOL)upgrade { //dbg msg os_log_debug(logHandle, "function '%s' invoked", __PRETTY_FUNCTION__); return [self install]; } //unistall -(BOOL)uninstall { //flag BOOL errors = NO; //error NSError* error = nil; //app NSString* app = [@"/Applications" stringByAppendingPathComponent:APP_NAME]; //dbg msg os_log_debug(logHandle, "function '%s' invoked", __PRETTY_FUNCTION__); //tell ext. to uninstall // remove rules, etc, etc if(YES != [xpcDaemonClient uninstall]) { //err msg os_log_error(logHandle, "ERROR: daemon's XPC uninstall logic"); //set flag errors = YES; //but continue onwards } //first, remove login item toggleLoginItem([NSURL fileURLWithPath:app], ACTION_UNINSTALL_FLAG); //quit (other) LuLus, network monitor, extension [self quit]; //app found in /Apps? if(YES == [NSFileManager.defaultManager fileExistsAtPath:app]) { //remove if(YES != [NSFileManager.defaultManager removeItemAtPath:app error:&error]) { //set flag errors = YES; //err msg os_log_error(logHandle, "ERROR: failed to remove %{public}@ (error: %{public}@)", app, error); //but continue onwards } //dbg msg else { os_log_debug(logHandle, "removed %{public}@", app); } } //dbg msg os_log_debug(logHandle, "uninstalling completed (with any errors? %d)", errors); return !errors; } //quit // and optionally uninstall -(void)quit { //extension Extension* extension = nil; //source NSString* source = nil; //copy in /Apps NSString* copy = nil; //running copy NSRunningApplication* runningCopy = nil; //error NSError* error = nil; //wait semaphore dispatch_semaphore_t semaphore = 0; //dbg msg os_log_debug(logHandle, "function '%s' invoked", __PRETTY_FUNCTION__); //init extension object extension = [[Extension alloc] init]; //init source source = NSBundle.mainBundle.bundlePath; //init copy copy = [@"/Applications" stringByAppendingPathComponent:APP_NAME]; //end LuLu // besides this running instance for(NSDictionary* lulu in findProcesses(@"LuLu")) { //pid NSNumber* pid = 0; //extract pid pid = lulu[KEY_PROCESS_ID]; //skip self if(pid.intValue == getpid()) { //skip continue; } //dbg msg os_log_debug(logHandle, "terminating %{public}@", lulu); //kill kill(pid.intValue, SIGKILL); } //terminate NQ [self terminateNetworkMonitor]; //need to stop extension? if(YES == [extension isExtensionRunning]) { //dbg msg os_log_debug(logHandle, "extension running, will deactivate..."); //have to be running from /Applications for this to work // so if we're not there, spawn a copy to exectute this logic if(YES != [source isEqualToString:copy]) { //dbg msg os_log_debug(logHandle, "will spawn copy from /Applications"); //any existing? if(YES == [NSFileManager.defaultManager fileExistsAtPath:copy]) { //remove if(YES != [NSFileManager.defaultManager removeItemAtPath:copy error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to remove %{public}@ (error: %{public}@)", copy, error); goto bail; } } //copy self into /Applications if(YES != [NSFileManager.defaultManager copyItemAtPath:source toPath:copy error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to move %{public}@ to %{public}@ (error: %{public}@)", source, copy, error); goto bail; } //dbg msg os_log_debug(logHandle, "launching copy %{public}@, to deactivate extension", copy); //launch copy runningCopy = [NSWorkspace.sharedWorkspace launchApplicationAtURL:[NSURL fileURLWithPath:copy] options:0 configuration:[NSDictionary dictionaryWithObject:@[@"-quit"] forKey:NSWorkspaceLaunchConfigurationArguments] error:&error]; if(nil == runningCopy) { //err msg os_log_error(logHandle, "ERROR: failed to launch copy, %{public}@, (error: %{public}@)", copy, error); goto bail; } //wait till copy exits while(YES != runningCopy.isTerminated) { [NSThread sleepForTimeInterval:0.1]; } //dbg msg os_log_debug(logHandle, "copy terminated, will delete"); //delete copy if(YES != [NSFileManager.defaultManager removeItemAtPath:copy error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to remove %{public}@ (error: %{public}@)", copy, error); goto bail; } //dbg msg os_log_debug(logHandle, "removed copy %{public}@", copy); } //(now) running from /Apps // go ahead and remove extension else { //init wait semaphore semaphore = dispatch_semaphore_create(0); //kick off extension activation request [extension toggleExtension:ACTION_DEACTIVATE reply:^(NSError* error) { //toggled ok? if(!error) { //dbg msg os_log_debug(logHandle, "extension deactivated"); } //failed? else { //err msg os_log_error(logHandle, "ERROR: failed to deactivate extension"); } //signal semaphore dispatch_semaphore_signal(semaphore); }]; //dbg msg os_log_debug(logHandle, "waiting system extension deactivation..."); //wait for extension semaphore dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //dbg msg os_log_debug(logHandle, "extension event triggered"); } } bail: return; } //terminate network monitor // unless its the non-LuLu version -(void)terminateNetworkMonitor { //find match // will check if LuLu's, then will terminate for(NSRunningApplication* networkMonitor in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.objective-see.Netiquette"]) { //non LuLu instance? if(YES != [networkMonitor.bundleURL.path hasPrefix:NSBundle.mainBundle.resourcePath]) continue; //dbg msg os_log_debug(logHandle, "terminating network monitor: %{public}@", networkMonitor); //terminate [networkMonitor terminate]; } return; } @end ================================================ FILE: LuLu/App/Extension.h ================================================ // // Extension.h // LuLu // // Created by Patrick Wardle on 9/11/20. // Copyright (c) 2020 Objective-See. All rights reserved. // @import OSLog; @import Foundation; @import NetworkExtension; @import SystemExtensions; typedef void(^replyBlockType)(NSError*); @interface Extension : NSObject /* PROPERTIES */ //reply @property(nonatomic, copy)replyBlockType replyBlock; /* METHODS */ //submit request to toggle extension -(void)toggleExtension:(NSUInteger)action reply:(replyBlockType)reply; //check if extension is running -(BOOL)isExtensionRunning; //activate/deactive network extension -(BOOL)toggleNetworkExtension:(NSUInteger)action; //get network extension's status -(BOOL)isNetworkExtensionEnabled; @end ================================================ FILE: LuLu/App/Extension.m ================================================ // // Extension.m // LuLu // // Created by Patrick Wardle on 9/11/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #import "consts.h" #import "Extension.h" #import "utilities.h" #import "AppDelegate.h" /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation Extension //submit request to toggle system extension -(void)toggleExtension:(NSUInteger)action reply:(replyBlockType)reply { //request OSSystemExtensionRequest* request = nil; //dbg msg os_log_debug(logHandle, "toggling extension (action: %lu)", (unsigned long)action); //save reply self.replyBlock = reply; //activation request if(ACTION_ACTIVATE == action) { //dbg msg os_log_debug(logHandle, "creating activation request"); //init request request = [OSSystemExtensionRequest activationRequestForExtension:EXT_BUNDLE_ID queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)]; } //deactivation request else { //dbg msg os_log_debug(logHandle, "creating deactivation request"); //init request request = [OSSystemExtensionRequest deactivationRequestForExtension:EXT_BUNDLE_ID queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)]; } //sanity check if(!request) { os_log_error(logHandle, "ERROR: failed to create request for extension"); //call reply self.replyBlock([NSError errorWithDomain:@BUNDLE_ID code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed to create system extension request"}]); goto bail; } //set delegate request.delegate = self; //dbg msg os_log_debug(logHandle, "submitting request"); //submit request [OSSystemExtensionManager.sharedManager submitRequest:request]; //dbg msg os_log_debug(logHandle, "submitting request returned..."); bail: return; } //check if extension is running -(BOOL)isExtensionRunning { return !![findProcesses(EXT_BUNDLE_ID) count]; } //get network extension's status -(BOOL)isNetworkExtensionEnabled { return NEFilterManager.sharedManager.isEnabled; } //activate/deactive network extension -(BOOL)toggleNetworkExtension:(NSUInteger)action { //flag BOOL toggled = NO; //error __block BOOL wasError = NO; //config NEFilterProviderConfiguration* config = nil; //wait semaphore dispatch_semaphore_t semaphore = 0; //init wait semaphore semaphore = dispatch_semaphore_create(0); //dbg msg os_log_debug(logHandle, "toggling network extension: %lu", (unsigned long)action); //load prefs [NEFilterManager.sharedManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) { //err? if(nil != error) { //set flag wasError = YES; //err msg os_log_error(logHandle, "ERROR: 'loadFromPreferencesWithCompletionHandler' failed with %{public}@", error); } //signal semaphore dispatch_semaphore_signal(semaphore); }]; //dbg msg os_log_debug(logHandle, "waiting for network extension configuration..."); //wait for request to complete dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //error? if(YES == wasError) goto bail; //dbg msg os_log_debug(logHandle, "loaded current filter configuration for the network extension"); //activate? // create new config, configure, save if(ACTION_ACTIVATE == action) { //dbg msg os_log_debug(logHandle, "activating network extension..."); //already enabled // good to go already if(NEFilterManager.sharedManager.enabled == YES) { os_log_debug(logHandle, "network extension already enabled; skipping save"); //done toggled = YES; goto bail; } //init config config = [[NEFilterProviderConfiguration alloc] init]; //don't care about packets config.filterPackets = NO; //filter sockets config.filterSockets = YES; //set config NEFilterManager.sharedManager.providerConfiguration = config; //set flag NEFilterManager.sharedManager.enabled = YES; } //deactivate // just set 'enabled' flag to NO else { //dbg msg os_log_debug(logHandle, "deactivating network extension..."); //set flag NEFilterManager.sharedManager.enabled = NO; } //save preferences { [NEFilterManager.sharedManager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) { //error? if(nil != error) { //set flag wasError = YES; //err msg os_log_error(logHandle, "ERROR: 'saveToPreferencesWithCompletionHandler' failed with %{public}@", error); } //signal semaphore dispatch_semaphore_signal(semaphore); }]; } //dbg msg os_log_debug(logHandle, "waiting for network extension configuration to save..."); //wait for request to complete dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //error? if(YES == wasError) goto bail; //dbg msg os_log_debug(logHandle, "saved current filter configuration for the network extension"); //happy toggled = YES; bail: return toggled; } #pragma mark - #pragma mark OSSystemExtensionRequest delegate methods //replace delegate method // always replaces, so return 'OSSystemExtensionReplacementActionReplace' -(OSSystemExtensionReplacementAction)request:(nonnull OSSystemExtensionRequest *)request actionForReplacingExtension:(nonnull OSSystemExtensionProperties *)existing withExtension:(nonnull OSSystemExtensionProperties *)ext { //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@, %{public}@ -> %{public}@", __PRETTY_FUNCTION__, request.identifier, existing.bundleShortVersion, ext.bundleShortVersion); return OSSystemExtensionReplacementActionReplace; } //error delegate method -(void)request:(nonnull OSSystemExtensionRequest *)request didFailWithError:(nonnull NSError *)error { //err msg os_log_error(logHandle, "ERROR: method '%s' invoked with %{public}@, %{public}@", __PRETTY_FUNCTION__, request, error); //invoke reply self.replyBlock(error); return; } //finish delegate method // install request? now can activate network ext // uninstall request? now can complete uninstall -(void)request:(nonnull OSSystemExtensionRequest *)request didFinishWithResult:(OSSystemExtensionRequestResult)result { //happy NSError* error = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@, %ld", __PRETTY_FUNCTION__, request, (long)result); //issue/error? if(OSSystemExtensionRequestCompleted != result) { //err msg os_log_error(logHandle, "ERROR: result %ld is an unexpected result for system extension request", (long)result); //set error error = [NSError errorWithDomain:@BUNDLE_ID code:result userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"System extension request failed with result: %ld", (long)result], NSLocalizedFailureReasonErrorKey: @"Unexpected result from system extension request", }]; } //reply self.replyBlock(error); return; } //user approval delegate // if this isn't the first time launch, will alert user to approve -(void)requestNeedsUserApproval:(nonnull OSSystemExtensionRequest *)request { //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@", __PRETTY_FUNCTION__, request); //not user launched? // show alert on desktop if(YES != launchedByUser()) { //on main thread // check and invoke dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //not first time? // show alert telling user to approve extension if(YES != [((AppDelegate*)[[NSApplication sharedApplication] delegate]) isFirstTime]) { //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"LuLu's Network Extension Is Not Running", @"LuLu's Network Extension Is Not Running"), NSLocalizedString(@"Extensions must be manually approved via System Settings (General > Login Items & Extensions > Network Extensions).",@"Extensions must be manually approved via System Settings (General > Login Items & Extensions > Network Extensions)."), @[NSLocalizedString(@"OK", @"OK")]); } }); } return; } @end ================================================ FILE: LuLu/App/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSUIElement NSHumanReadableCopyright Copyright (c) 2020 Objective-See. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplicationKeyEvents NSSupportsAutomaticTermination NSSupportsSuddenTermination ================================================ FILE: LuLu/App/InfoPlist.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "CFBundleName" : { "comment" : "Bundle name", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } } } }, "NSHumanReadableCopyright" : { "comment" : "Copyright (human-readable)", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Copyright (c) 2020 Objective-See. Alle Rechte vorbehalten." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Copyright (c) 2020 Objective-See. All rights reserved." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Copyright (c) 2020 Objective-See. Todos los derechos reservados." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Copyright (c) 2020 Objective-See. 모든 권리 보유." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Copyright (c) 2020 Objective-See. Todos os direitos reservados." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Telif hakkı © 2020 Objective-See. Tüm hakları saklıdır." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کاپی رائٹ (c) 2020 Objective-See۔ تمام حقوق محفوظ ہیں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "版权所有 (c) 2020 Objective-See. 保留一切权利。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版權所有 (c) 2020 Objective-See. 保留一切權利。" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/ItemPathsWindowController.h ================================================ // // ItemPathsWindowController.h // LuLu // // Created by Patrick Wardle on 9/19/20. // Copyright (c) 2020 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "Rule.h" NS_ASSUME_NONNULL_BEGIN @interface ItemPathsWindowController : NSWindowController //title @property (weak) IBOutlet NSTextField* heading; //item's rules @property(nonatomic, retain)NSDictionary* item; //item paths @property (weak) IBOutlet NSTextField* itemPaths; //close button @property (weak) IBOutlet NSButton* closeButton; @end NS_ASSUME_NONNULL_END ================================================ FILE: LuLu/App/ItemPathsWindowController.m ================================================ // // ItemPathsWindowController.m // LuLu // // Created by Patrick Wardle on 9/19/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #import "consts.h" #import "signing.h" #import "utilities.h" #import "XPCDaemonClient.h" #import "ItemPathsWindowController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; @implementation ItemPathsWindowController @synthesize item; //generate and show paths -(void)windowDidLoad { //rule Rule* rule = nil; //super [super windowDidLoad]; //unique paths NSMutableSet* paths = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //extract (first) rule rule = [self.item[KEY_RULES] firstObject]; //set heading if 'normal' rule if(!rule.isGlobal.boolValue && !rule.isDirectory.boolValue) { //set heading self.heading.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Path(s) for %@:", nil), rule.name]; } //reset heading else { self.heading.stringValue = NSLocalizedString(@"Path(s):", nil); } //get paths paths = [self getPaths]; //each rule's path for(NSString* path in paths) { if(0 == self.itemPaths.stringValue.length) { self.itemPaths.stringValue = [NSString stringWithFormat:@"▪ %@", path]; } //otherwise // append to existing else { self.itemPaths.stringValue = [NSString stringWithFormat:@"%@\r\n▪ %@", self.itemPaths.stringValue, path]; } } //make close button first responder [self.window makeFirstResponder:self.closeButton]; return; } //get all unique paths for item // both from rules, but also from system (via bundle ID) -(NSMutableSet*)getPaths { //(first) rule Rule* rule = nil; //items NSArray* items = nil; //paths NSMutableSet* paths = nil; //item bundle id NSString* bundleID = nil; //signing info NSMutableDictionary* signingInfo = nil; //init paths = [NSMutableSet set]; //extract (first) rule rule = [self.item[KEY_RULES] firstObject]; //global rule? if(YES == rule.isGlobal.boolValue) { //set message [paths addObject:NSLocalizedString(@"Global Rules apply to all paths", @"Global Rules apply to all paths")]; //done goto bail; } //directory rule? if(YES == rule.isDirectory.boolValue) { //set message [paths addObject:NSLocalizedString(@"Directory Rules apply to all items within the directory", @"Directory Rules apply to all items within the directory")]; //done goto bail; } //first add rule's path if(nil != rule.path) { //add [paths addObject:rule.path]; } //add any external paths [paths unionSet:item[KEY_PATHS]]; //grab bundle id bundleID = rule.csInfo[KEY_CS_ID]; //add other items on system // with same bundle id *and* cs info, as these will match if(nil != bundleID) { //get matching apps items = (__bridge NSArray *)(LSCopyApplicationURLsForBundleIdentifier((__bridge CFStringRef _Nonnull)(bundleID), nil)); //get each item's binary for(NSURL* item in items) { //path NSString* path = nil; //attempt to get path via bundle path = getBundleExecutable(item.path); //likely not bundle if(0 == path.length) { path = item.path; } //sanity check if(0 == path.length) { //skip continue; } //extract signing info and check // note: use item's path, to match rule signingInfo = extractSigningInfo(0, item.path, kSecCSDefaultFlags); if(YES != matchesCSInfo(self.item[KEY_CS_INFO], signingInfo)) { //dbg msg os_log_debug(logHandle, "rule's code signing info %{public}@ doesn't match item's %{public}@", self.item[KEY_CS_INFO], signingInfo); //skip continue; } //add [paths addObject:path]; } } bail: return paths; } //close -(IBAction)closeButtonHandler:(id)sender { //dbg msg os_log_debug(logHandle, "user clicked: %{public}@", ((NSButton*)sender).title); //close & return NSModalResponseCancel [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseOK]; return; } @end ================================================ FILE: LuLu/App/NSApplicationKeyEvents.h ================================================ // // file: NSApplicationKeyEvents.h // project: LuLu // description: adds support for keyboard shortcuts (header) // // created by Patrick Wardle // copyright (c) 2020 Objective-See. All rights reserved. // @import Cocoa; @interface NSApplicationKeyEvents : NSApplication @end ================================================ FILE: LuLu/App/NSApplicationKeyEvents.m ================================================ // // file: NSApplicationKeyEvents.h // project: LuLu // description: adds support for keyboard shortcuts (header) // // created by Patrick Wardle // copyright (c) 2020 Objective-See. All rights reserved. // #import "consts.h" #import "AppDelegate.h" #import "NSApplicationKeyEvents.h" @implementation NSApplicationKeyEvents //to enable copy/paste etc even though we don't have an 'Edit' menu // details: http://stackoverflow.com/questions/970707/cocoa-keyboard-shortcuts-in-dialog-without-an-edit-menu -(void)sendEvent:(NSEvent*)event { //only care about key down if(NSEventTypeKeyDown != event.type) { //bail goto bail; } //delete? // in Rules window/table, delete row if( (NSDeleteCharacter == [event.charactersIgnoringModifiers characterAtIndex:0]) && (YES == [event.window.identifier isEqualToString:@"Rules"]) && (YES == [event.window.firstResponder.className isEqualToString:@"RulesTable"]) ) { //delete rule [[((AppDelegate*)[[NSApplication sharedApplication] delegate]) rulesWindowController] deleteRule:nil]; return; } //otherwise // ...only care about key down + command if(NSEventModifierFlagCommand != (event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask)) { //bail goto bail; } //+c (copy) if(YES == [event.charactersIgnoringModifiers isEqualToString:@"c"]) { //copy if(YES == [self sendAction:@selector(copy:) to:nil from:self]) { return; } } //+v (paste) else if ([event.charactersIgnoringModifiers isEqualToString:@"v"]) { //paste if(YES == [self sendAction:@selector(paste:) to:nil from:self]) { return; } } //+x (cut) else if ([event.charactersIgnoringModifiers isEqualToString:@"x"]) { //cut if(YES == [self sendAction:@selector(cut:) to:nil from:self]) { return; } } //+a (select all) else if([event.charactersIgnoringModifiers isEqualToString:@"a"]) { //select if(YES == [self sendAction:@selector(selectAll:) to:nil from:self]) { return; } } //+h (hide window) else if([event.charactersIgnoringModifiers isEqualToString:@"h"]) { //hide if(YES == [self sendAction:@selector(hide:) to:nil from:self]) { return; } } //+m (minimize window) else if([event.charactersIgnoringModifiers isEqualToString:@"m"]) { //minimize [NSApplication.sharedApplication.keyWindow miniaturize:nil]; return; } //+w (close window) // unless its an alert ...need response! else if( ([event.charactersIgnoringModifiers isEqualToString:@"w"]) && (YES != [event.window.identifier isEqualToString:@"Alert"]) ) { //close [NSApplication.sharedApplication.keyWindow close]; return; } //+f (find, but only in rules window) else if( ([event.charactersIgnoringModifiers isEqualToString:@"f"]) && (YES == [event.window.identifier isEqualToString:@"Rules"]) ) { //iterate over all toolbar items // ...find rule search field, and select for(NSToolbarItem* item in NSApplication.sharedApplication.keyWindow.toolbar.items) { //not search field? skip if(RULE_SEARCH_FIELD != item.tag) continue; //and make it first responder [NSApplication.sharedApplication.keyWindow makeFirstResponder:item.view]; //done return; } } //+, (show preferences) else if([event.charactersIgnoringModifiers isEqualToString:@","]) { //show [((AppDelegate*)[[NSApplication sharedApplication] delegate]) showPreferences:nil]; return; } bail: //super [super sendEvent:event]; return; } @end ================================================ FILE: LuLu/App/ParentsWindowController.h ================================================ // // file: ParentsWindowController.h // project: lulu (login item) // description: window controller for process heirachy (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @interface ParentsWindowController : NSViewController { } /* PROPERTIES */ //process hierarchy @property (nonatomic, retain)NSArray* processHierarchy; /* METHODS */ @end ================================================ FILE: LuLu/App/ParentsWindowController.m ================================================ // // file: ParentsWindowController.m // project: lulu (login item) // description: window controller for process heirachy // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "ParentsWindowController.h" @implementation ParentsWindowController //automatically invoked // return number of children, simply either 1, or 0 (for last item) -(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { //# of kids // defaults to 1 NSInteger kidCount = 1; //for last item // no more kids... if( (1 != self.processHierarchy.count) && ([((NSDictionary*)item)[KEY_INDEX] integerValue] == self.processHierarchy.count-1) ) { //no kids kidCount = 0; } return kidCount; } //automatically invoked to determine if item is expandable // always the case, except for the last item -(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { //flag // ->defaults to yes BOOL isExpandable = YES; //for last item // no kids, so obv. no expandable if([((NSDictionary*)item)[KEY_INDEX] integerValue] == self.processHierarchy.count-1) { //last one isExpandable = NO; } return isExpandable; } //automatically invoked to get the child item -(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { //item id itemForRow = nil; //item is nil for root // just provide root item if(nil == item) { //first item itemForRow = self.processHierarchy[0]; } //other items // return *their* child! else { //child at index itemForRow = self.processHierarchy[[((NSDictionary*)item)[KEY_INDEX] integerValue]+1]; } return itemForRow; } //automatically invoked to get the object for the row // return items name/pid -(id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { //row value NSString* rowValue = nil; //init string for row // process name + pid // note: if this format changes, also change row width calculation in AlertWindowController! rowValue = [NSString stringWithFormat:NSLocalizedString(@"%@ (pid: %@)",@"%@ (pid: %@)"), ((NSDictionary*)item)[KEY_PROCESS_NAME], ((NSDictionary*)item)[@"pid"]]; return rowValue; } @end ================================================ FILE: LuLu/App/PrefsWindowController.h ================================================ // // file: PrefsWindowController.h // project: lulu (main app) // description: preferences window controller (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "XPCDaemonClient.h" #import "UpdateWindowController.h" /* CONSTS */ //rules view #define TOOLBAR_RULES 0 //modes view #define TOOLBAR_MODES 1 //update view #define TOOLBAR_LISTS 2 //profiles view #define TOOLBAR_PROFILES 3 //update view #define TOOLBAR_UPDATE 4 //to select, need string ID #define TOOLBAR_RULES_ID @"Rules" #define TOOLBAR_PROFILES_ID @"Profiles" @interface PrefsWindowController : NSWindowController /* PROPERTIES */ //preferences @property(nonatomic, retain)NSDictionary* preferences; //toolbar @property (weak) IBOutlet NSToolbar* toolbar; /* RULES */ //rules prefs view @property (weak) IBOutlet NSView* rulesView; //show rules button @property (weak) IBOutlet NSButton* showRulesButton; /* MODES */ //modes view @property (strong) IBOutlet NSView* modesView; //passive mode action ...allow or block? @property (weak) IBOutlet NSPopUpButton* passiveModeAction; //passive mode rules ...create, or not? @property (weak) IBOutlet NSPopUpButton* passiveModeRules; //(block/allow) lists view @property (strong) IBOutlet NSView *listsView; //allow list @property (weak) IBOutlet NSTextField *allowList; //select allow list button @property (weak) IBOutlet NSButton *selectAllowListButton; //block list @property (weak) IBOutlet NSTextField* blockList; //select block list button @property (weak) IBOutlet NSButton* selectBlockListButton; //profiles table @property (weak) IBOutlet NSTableView *profilesTable; /* PROFILES VIEW */ //profiles view @property (strong) IBOutlet NSView* profilesView; //profiles @property(nonatomic, retain)NSMutableArray* profiles; //selected profile @property(nonatomic, retain)NSString* selectedProfile; //add profile sheet @property (strong) IBOutlet NSPanel* addProfileSheet; //continue/next button @property (weak) IBOutlet NSButton* continueProfileButton; //current view @property (strong) NSView* currentProfileSubview; //profile name label @property (weak) IBOutlet NSTextField* profileNameLabel; //profile name view @property (strong) IBOutlet NSView* profileNameView; //new profile name @property(nonatomic, retain)NSString* profileName; //profile preferences @property(nonatomic, retain)NSMutableDictionary* profilePreferences; //profile views enum profileViews { profileName = 0, profileRules, profileModes, profileLists, profileUpdates, }; /* UPDATE VIEW */ //update view @property (weak) IBOutlet NSView* updateView; //update button @property (weak) IBOutlet NSButton* updateButton; //update indicator (spinner) @property (weak) IBOutlet NSProgressIndicator* updateIndicator; //update label @property (weak) IBOutlet NSTextField* updateLabel; //update window controller @property(nonatomic, retain)UpdateWindowController* updateWindowController; //added view @property (nonatomic) BOOL viewWasAdded; /* METHODS */ //toolbar button handler -(IBAction)toolbarButtonHandler:(id)sender; //switch to tab -(void)switchTo:(NSString*)itemID; //button handler for all preference buttons -(IBAction)togglePreference:(id)sender; //reload UI -(void)reload; @end ================================================ FILE: LuLu/App/PrefsWindowController.m ================================================ // // file: PrefsWindowController.h // project: lulu (main app) // description: preferences window controller (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "Update.h" #import "utilities.h" #import "AppDelegate.h" #import "PrefsWindowController.h" #import "UpdateWindowController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; @implementation PrefsWindowController @synthesize toolbar; @synthesize modesView; @synthesize rulesView; @synthesize updateView; @synthesize updateWindowController; //'allow apple' button #define BUTTON_ALLOW_APPLE 1 //'allow installed' button #define BUTTON_ALLOW_INSTALLED 2 //'allow dns' button #define BUTTON_ALLOW_DNS 3 //'allow localhost' button #define BUTTON_ALLOW_LOCALHOST 4 //'allow iOS simulator apps' mode button #define BUTTON_ALLOW_SIMULATOR 5 //'passive mode' button #define BUTTON_PASSIVE_MODE 6 //'block mode' button #define BUTTON_BLOCK_MODE 7 //'no-icon mode' button #define BUTTON_NO_ICON_MODE 8 //'no-VT mode' button #define BUTTON_NO_VT_MODE 9 //'use allow list' button #define BUTTON_USE_ALLOW_LIST 10 //'use block list' button #define BUTTON_USE_BLOCK_LIST 11 //'update mode' button #define BUTTON_NO_UPDATE_MODE 12 //'passive mode' actions #define BUTTON_PASSIVE_MODE_ACTION_ALLOW 0 #define BUTTON_PASSIVE_MODE_ACTION_BLOCK 1 //init 'general' view // add it, and make it selected -(void)awakeFromNib { //set subtitle [self setSubTitle]; //get prefs self.preferences = [xpcDaemonClient getPreferences]; return; } //set subtitle to current profile -(void)setSubTitle { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //current profile NSString* currentProfile = [xpcDaemonClient getCurrentProfile]; //have profile? if(0 != currentProfile.length) { //add subtitle if (@available(macOS 11.0, *)) { self.window.subtitle = [NSString stringWithFormat:NSLocalizedString(@"Current Profile: %@",@"Current Profile: %@"), currentProfile]; } } //set to default else { //set if (@available(macOS 11.0, *)) { self.window.subtitle = NSLocalizedString(@"Current Profile: Default",@"Current Profile: Default"); } } return; } //switch to tab -(void)switchTo:(NSString*)itemID { //predicate NSPredicate* predicate = [NSPredicate predicateWithFormat:@"itemIdentifier == %@", itemID]; //item NSToolbarItem* item = [[self.toolbar.items filteredArrayUsingPredicate:predicate] firstObject]; //dbg msg os_log_debug(logHandle, "item '%@' -> %{public}@", itemID, item); //select [self toolbarButtonHandler:item]; [self.toolbar setSelectedItemIdentifier:itemID]; return; } //toolbar view handler // toggle view based on user selection -(IBAction)toolbarButtonHandler:(id)sender { //view NSView* view = nil; //dbg msg os_log_debug(logHandle, "%s invoked with %{public}@", __PRETTY_FUNCTION__, sender); //when we've prev added a view // remove the prev view cuz adding a new one if(YES == self.viewWasAdded) { //dbg msg os_log_debug(logHandle, "removing previous view..."); //remove [[[self.window.contentView subviews] lastObject] removeFromSuperview]; } //assign view switch(((NSToolbarItem*)sender).tag) { //rules case TOOLBAR_RULES: //set view view = self.rulesView; //show self.showRulesButton.hidden = NO; //set 'apple allowed' button state ((NSButton*)[view viewWithTag:BUTTON_ALLOW_APPLE]).state = [self.preferences[PREF_ALLOW_APPLE] boolValue]; //set 'installed allowed' button state ((NSButton*)[view viewWithTag:BUTTON_ALLOW_INSTALLED]).state = [self.preferences[PREF_ALLOW_INSTALLED] boolValue]; //set 'allow dns' button state ((NSButton*)[view viewWithTag:BUTTON_ALLOW_DNS]).state = [self.preferences[PREF_ALLOW_DNS] boolValue]; //set 'allow localhost' button state ((NSButton*)[view viewWithTag:BUTTON_ALLOW_LOCALHOST]).state = [self.preferences[PREF_ALLOW_LOCALHOST] boolValue]; //set 'allow simulator apps' button ((NSButton*)[view viewWithTag:BUTTON_ALLOW_SIMULATOR]).state = [self.preferences[PREF_ALLOW_SIMULATOR] boolValue]; break; //modes case TOOLBAR_MODES: //set view view = self.modesView; //set 'passive mode' button state ((NSButton*)[view viewWithTag:BUTTON_PASSIVE_MODE]).state = [self.preferences[PREF_PASSIVE_MODE] boolValue]; //set 'passive mode' action [self.passiveModeAction selectItemAtIndex: [self.preferences[PREF_PASSIVE_MODE_ACTION] integerValue]]; //set 'passive mode' rules [self.passiveModeRules selectItemAtIndex: [self.preferences[PREF_PASSIVE_MODE_RULES] integerValue]]; //set 'block mode' button state ((NSButton*)[view viewWithTag:BUTTON_BLOCK_MODE]).state = [self.preferences[PREF_BLOCK_MODE] boolValue]; //set 'no icon' button state ((NSButton*)[view viewWithTag:BUTTON_NO_ICON_MODE]).state = [self.preferences[PREF_NO_ICON_MODE] boolValue]; //set 'no VT icon' button state ((NSButton*)[view viewWithTag:BUTTON_NO_VT_MODE]).state = [self.preferences[PREF_NO_VT_MODE] boolValue]; break; //lists case TOOLBAR_LISTS: //set view view = self.listsView; //set 'allow list' button state ((NSButton*)[view viewWithTag:BUTTON_USE_ALLOW_LIST]).state = [self.preferences[PREF_USE_ALLOW_LIST] boolValue]; //is there a allow list? ...set! if(0 != [self.preferences[PREF_ALLOW_LIST] length]) { //set self.allowList.stringValue = self.preferences[PREF_ALLOW_LIST]; } //set 'browse' button state self.selectAllowListButton.enabled = [self.preferences[PREF_USE_ALLOW_LIST] boolValue]; //set allow list input state self.allowList.enabled = [self.preferences[PREF_USE_ALLOW_LIST] boolValue]; //set 'block list' button state ((NSButton*)[view viewWithTag:BUTTON_USE_BLOCK_LIST]).state = [self.preferences[PREF_USE_BLOCK_LIST] boolValue]; //is there a block list? ...set! if(0 != [self.preferences[PREF_BLOCK_LIST] length]) { //set self.blockList.stringValue = self.preferences[PREF_BLOCK_LIST]; } //set 'browse' button state self.selectBlockListButton.enabled = [self.preferences[PREF_USE_BLOCK_LIST] boolValue]; //set block list input state self.blockList.enabled = [self.preferences[PREF_USE_BLOCK_LIST] boolValue]; break; //profiles case TOOLBAR_PROFILES: //set view view = self.profilesView; //send XPC msg to daemon get profiles self.profiles = [xpcDaemonClient getProfiles]; //manually add default at start [self.profiles insertObject:@"Default" atIndex:0]; //dbg msg os_log_debug(logHandle, "list of profiles: %{public}@", self.profiles); //reload table [self.profilesTable reloadData]; break; //update case TOOLBAR_UPDATE: //set view view = self.updateView; //set 'update' button state ((NSButton*)[view viewWithTag:BUTTON_NO_UPDATE_MODE]).state = [self.preferences[PREF_NO_UPDATE_MODE] boolValue]; //show self.updateButton.hidden = NO; //(re)set update label self.updateLabel.stringValue = @""; break; default: return; } // Resize window to fit the view’s height (keeping top edge fixed) NSRect windowFrame = self.window.frame; CGFloat newHeight = view.frame.size.height + 50 + 10; //toolbar + a bit extra CGFloat newWidth = view.frame.size.width; CGFloat deltaY = NSMaxY(windowFrame) - newHeight; [self.window setFrame:NSMakeRect(windowFrame.origin.x, deltaY, newWidth, newHeight) display:YES]; // Position view so its top aligns with the window’s contentView top NSView *container = self.window.contentView; NSRect viewFrame = view.frame; viewFrame.origin.y = container.bounds.size.height - viewFrame.size.height; viewFrame.origin.x = 0; view.frame = viewFrame; //add to window [self.window.contentView addSubview:view]; //set self.viewWasAdded = YES; bail: return; } //invoked when user toggles button // update preferences for that button/item -(IBAction)togglePreference:(id)sender { //preferences NSMutableDictionary* updatedPreferences = nil; //button state NSNumber* state = nil; //in "add profile" mode // want to capture all the preferences if(YES == self.addProfileSheet.isVisible) { updatedPreferences = self.profilePreferences; } //otherwise // grab (just) updated preferences to send to daemon else { //init updatedPreferences = [NSMutableDictionary dictionary]; } //get button state state = [NSNumber numberWithBool:((NSButton*)sender).state]; //set appropriate preference switch(((NSButton*)sender).tag) { //allow apple case BUTTON_ALLOW_APPLE: updatedPreferences[PREF_ALLOW_APPLE] = state; break; //allow installed case BUTTON_ALLOW_INSTALLED: updatedPreferences[PREF_ALLOW_INSTALLED] = state; break; //allow dns traffic case BUTTON_ALLOW_DNS: updatedPreferences[PREF_ALLOW_DNS] = state; break; //allow localhost host case BUTTON_ALLOW_LOCALHOST: updatedPreferences[PREF_ALLOW_LOCALHOST] = state; break; //allow simulator apps case BUTTON_ALLOW_SIMULATOR: updatedPreferences[PREF_ALLOW_SIMULATOR] = state; break; //use block list case BUTTON_USE_ALLOW_LIST: //set state updatedPreferences[PREF_USE_ALLOW_LIST] = state; //disable? // remove allow list too if(NSControlStateValueOff == state.longValue) { //unset updatedPreferences[PREF_ALLOW_LIST] = @""; //clear self.allowList.stringValue = @""; } //set allow list input state self.allowList.enabled = (NSControlStateValueOn == state.longValue); //set 'browse' button state self.selectAllowListButton.enabled = (NSControlStateValueOn == state.longValue); break; //use block list case BUTTON_USE_BLOCK_LIST: //set updatedPreferences[PREF_USE_BLOCK_LIST] = state; //disable? // remove block list too if(NSControlStateValueOff == state.longValue) { //unset updatedPreferences[PREF_BLOCK_LIST] = @""; //clear self.blockList.stringValue = @""; } //set block list input state self.blockList.enabled = (NSControlStateValueOn == state.longValue); //set 'browse' button state self.selectBlockListButton.enabled = (NSControlStateValueOn == state.longValue); break; //passive mode case BUTTON_PASSIVE_MODE: //grab state updatedPreferences[PREF_PASSIVE_MODE] = state; //grab selected item of action updatedPreferences[PREF_PASSIVE_MODE_ACTION] = [NSNumber numberWithInteger:self.passiveModeAction.indexOfSelectedItem]; //grab selected item of rules updatedPreferences[PREF_PASSIVE_MODE_RULES] = [NSNumber numberWithInteger:self.passiveModeRules.indexOfSelectedItem]; break; //block mode case BUTTON_BLOCK_MODE: updatedPreferences[PREF_BLOCK_MODE] = state; //enable? // show alert if(NSControlStateValueOn == state.longValue) { //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Outgoing traffic will now be blocked.",@"Outgoing traffic will now be blocked."), NSLocalizedString(@"Note however:\r\n▪ Existing connections will not be impacted.\r\n▪ OS traffic (not routed thru LuLu) will not be blocked.",@"Note however:\r\n▪ Existing connections will not be impacted.\r\n▪ OS traffic (not routed thru LuLu) will not be blocked."), @[NSLocalizedString(@"OK", @"OK")]); } break; //no icon mode case BUTTON_NO_ICON_MODE: updatedPreferences[PREF_NO_ICON_MODE] = state; break; //no vt mode case BUTTON_NO_VT_MODE: updatedPreferences[PREF_NO_VT_MODE] = state; break; //no update mode case BUTTON_NO_UPDATE_MODE: updatedPreferences[PREF_NO_UPDATE_MODE] = state; break; default: break; } //logic for 'passive mode' action if(YES == [sender isEqualTo:self.passiveModeAction]) { //grab selected index updatedPreferences[PREF_PASSIVE_MODE_ACTION] = [NSNumber numberWithInteger:self.passiveModeAction.indexOfSelectedItem]; } //logic for 'passive mode' rules else if(YES == [sender isEqualTo:self.passiveModeRules]) { //grab selected index updatedPreferences[PREF_PASSIVE_MODE_RULES] = [NSNumber numberWithInteger:self.passiveModeRules.indexOfSelectedItem]; } //only process here if we're not in "add profile" mode if(YES != self.addProfileSheet.isVisible) { //send XPC msg to daemon to update prefs // returns (all/latest) prefs, which is what we want self.preferences = [xpcDaemonClient updatePreferences:updatedPreferences]; //call back into app to process // e.g. show/hide status bar icon, etc. [((AppDelegate*)[[NSApplication sharedApplication] delegate]) preferencesChanged:self.preferences]; } return; } //browse for select list -(IBAction)selectBlockOrAllowList:(id)sender { //'browse' panel NSOpenPanel *panel = nil; //init panel panel = [NSOpenPanel openPanel]; //allow files panel.canChooseFiles = YES; //start ...at desktop panel.directoryURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES) firstObject]]; //disable multiple selections panel.allowsMultipleSelection = NO; //show it // and wait for response if(NSModalResponseOK == [panel runModal]) { //allow list if(sender == self.selectAllowListButton) { //update ui self.allowList.stringValue = panel.URL.path; //dbg msg os_log_debug(logHandle, "user selected allow list: %{public}@", self.allowList.stringValue); //send XPC msg to daemon to update prefs self.preferences = [xpcDaemonClient updatePreferences:@{PREF_ALLOW_LIST:panel.URL.path}]; } //block list else if(sender == self.selectBlockListButton) { //update ui self.blockList.stringValue = panel.URL.path; //dbg msg os_log_debug(logHandle, "user selected block list: %{public}@", self.blockList.stringValue); //send XPC msg to daemon to update prefs self.preferences = [xpcDaemonClient updatePreferences:@{PREF_BLOCK_LIST:panel.URL.path}]; } //error else { //err msg os_log_error(logHandle, "ERROR: %{public}@ is an invalid sender", sender); } } return; } //invoked when block list path is (manually entered) -(IBAction)updateBlockList:(id)sender { //dbg msg os_log_debug(logHandle, "got 'update block list event' (value: %{public}@)", self.blockList.stringValue); //send XPC msg to daemon to update prefs // returns (all/latest) prefs, which is what we want self.preferences = [xpcDaemonClient updatePreferences:@{PREF_BLOCK_LIST:self.blockList.stringValue}]; return; } //reload current toolbar view (as profile changed) // ...by triggering a 'click' to our toolbar button handler -(void)reload { //dbg msg os_log_debug(logHandle, "%s invoked", __PRETTY_FUNCTION__); //grab (profile's) preferences self.preferences = [xpcDaemonClient getPreferences]; //(re)set subtitle [self setSubTitle]; //selected ID NSToolbarItemIdentifier selectedID = self.toolbar.selectedItemIdentifier; //selected item NSToolbarItem* toolbarItem = [[self.toolbar items] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"itemIdentifier == %@", selectedID]].firstObject; //trigger reload [self toolbarButtonHandler:toolbarItem]; return; } #pragma mark – Profile's table delegates //number of profiles - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return self.profiles.count; } //view for each column + row - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { //dequeue a cell NSTableCellView *cell = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self]; //first column // check button if we're looking at the current profile if(YES == [tableColumn.identifier isEqualToString:@"Current"]) { //current profile NSString* currentProfile = [xpcDaemonClient getCurrentProfile]; //select button NSButton* selectButton = (NSButton*)[cell viewWithTag:TABLE_ROW_SELECT_BTN_TAG]; //dbg msg os_log_debug(logHandle, "current row: %ld, current profile %{public}@, select button: %{public}@", (long)row, currentProfile, selectButton); //no profile? // select button/row zero (default) if( (0 == row) && (nil == currentProfile) ) { //dbg msg os_log_debug(logHandle, "enabling default button here..."); //select selectButton.state = NSControlStateValueOn; //select row too [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; } //profile matches current? else if(YES == [currentProfile isEqualToString:self.profiles[row]]) { //dbg msg os_log_debug(logHandle, "match, enabling button here..."); //select selectButton.state = NSControlStateValueOn; //select row too [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; } else { //turn off selectButton.state = NSControlStateValueOff; } } //add name // and customize delete button if ([tableColumn.identifier isEqualToString:@"Name"]) { //delete button NSButton* deleteButton = (NSButton*)[cell viewWithTag:TABLE_ROW_DELETE_BTN_TAG]; //add name cell.textField.stringValue = self.profiles[row]; //first row? // this is 'default' profile, so disable delete button if(0 == row) { //hide deleteButton.hidden = YES; } else { //show/enable deleteButton.hidden = NO; deleteButton.enabled = YES; } } return cell; } #pragma mark – Profile's button handlers //get profile name from current/selected row -(NSString*)profileFromTable:(id)sender { //profile path NSString* profile = nil; //index of row // either clicked or selected row NSInteger row = 0; //dbg msg os_log_debug(logHandle, "%s invoked", __PRETTY_FUNCTION__); //get row if(nil != sender) { //row from sender row = [self.profilesTable rowForView:sender]; } //otherwise get selected row else { //selected row row = self.profilesTable.selectedRow; } //get profile // index 0, is the default profile, which we want as nil if(row > 0 && row < self.profiles.count) { //get profile profile = self.profiles[row]; } //dbg msg os_log_debug(logHandle, "row: %ld, profile: %{public}@", (long)row, profile); return profile; } //'switch profile' button handler -(IBAction)switchProfile:(id)sender { //profile NSString* profile = nil; //dbg msg os_log_debug(logHandle, "%s invoked", __PRETTY_FUNCTION__); //get profile // can be 'nil' if default profile is selected profile = [self profileFromTable:sender]; //dbg msg os_log_debug(logHandle, "user wants to change profile to '%{public}@'", profile ? profile : @"Default"); //set profile via XPC [xpcDaemonClient setProfile:profile]; //tell app profiles changed // will grab profile's preferences too [((AppDelegate*)[[NSApplication sharedApplication] delegate]) profilesChanged]; //also tell app preferences changed [((AppDelegate*)[[NSApplication sharedApplication] delegate]) preferencesChanged:self.preferences]; //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Profile Switched", @"Profile Switched"), [NSString stringWithFormat:NSLocalizedString(@"Current profile is now: '%@'.", @"Current profile is now: '%@'."), nil != profile ? profile : NSLocalizedString(@"Default", @"Default")], @[NSLocalizedString(@"OK", @"OK")]); return; } //add profile button handler // show sheet for user to specify settings -(IBAction)addProfile:(id)sender { //dbg msg os_log_debug(logHandle, "%s invoked", __PRETTY_FUNCTION__); //init dictionary to collect preferences self.profilePreferences = [NSMutableDictionary dictionary]; //init/reset self.profileName = nil; //init/reset self.continueProfileButton.tag = 0; //remove any old view if(self.currentProfileSubview) { //remove current view [self.currentProfileSubview removeFromSuperview]; } //init current view (with profile name) self.currentProfileSubview = self.profileNameView; //add initial (profile name) view [self.addProfileSheet.contentView addSubview:self.currentProfileSubview]; //disable autoresizing mask self.currentProfileSubview.translatesAutoresizingMaskIntoConstraints = NO; //pin to top, leading, and trailing edges [NSLayoutConstraint activateConstraints:@[ [self.currentProfileSubview.topAnchor constraintEqualToAnchor:self.addProfileSheet.contentView.topAnchor], [self.currentProfileSubview.leadingAnchor constraintEqualToAnchor:self.addProfileSheet.contentView.leadingAnchor], [self.currentProfileSubview.trailingAnchor constraintEqualToAnchor:self.addProfileSheet.contentView.trailingAnchor] ]]; //reset button name self.continueProfileButton.title = NSLocalizedString(@"Next", @"Next"); //set profile name self.profileNameLabel.stringValue = @""; //show sheet for user to add profile [self.window beginSheet:self.addProfileSheet completionHandler:^(NSModalResponse returnCode) { //add profile? // and handle UI refreshes, etc if (returnCode == NSModalResponseOK) { //dbg msg os_log_debug(logHandle, "user wants to add profile '%{public}@'", self.profileName); //add profile via XPC [xpcDaemonClient addProfile:self.profileName preferences:self.profilePreferences]; //hide profile sheet [self.addProfileSheet orderOut:self]; //tell app profiles changed // will grab profile's preferences too [((AppDelegate*)[[NSApplication sharedApplication] delegate]) profilesChanged]; //tell app preferences changed [((AppDelegate*)[[NSApplication sharedApplication] delegate]) preferencesChanged:self.preferences]; //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Added Profile", @"Added Profile"), [NSString stringWithFormat:NSLocalizedString(@"New profile '%@' saved and activated.", @"New profile '%@' saved and activated."), self.profileName], @[NSLocalizedString(@"OK", @"OK")]); } //cancel else { //close sheet [self.addProfileSheet orderOut:self]; } }]; return; } //cancel creation of profile - (IBAction)cancelProfileButtonHandler:(id)sender { //end w/ cancel [self.window endSheet:self.addProfileSheet returnCode:NSModalResponseCancel]; return; } //show next view // note: each case is current view, going to next! -(IBAction)continueProfileButtonHandler:(NSButton*)sender { //switch on current view // last view, will add the profile switch (sender.tag) { //current view: name // setup next view: rules case profileName: { //check against empty if(!self.profileNameLabel.stringValue.length) { //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Invalid Profile Name", @"Invalid Profile Name"), NSLocalizedString(@"Profile name can't be blank", @"Profile name can't be blank"), @[NSLocalizedString(@"OK", @"OK")]); self.profileNameLabel.stringValue = @""; goto bail; } //check against 'Default' if(NSOrderedSame == [self.profileNameLabel.stringValue caseInsensitiveCompare:NSLocalizedString(@"Default", @"Default")]) { //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Invalid Profile Name", @"Invalid Profile Name"), NSLocalizedString(@"'Default' is a reserved profile name.", @"'Default' is a reserved profile name."), @[NSLocalizedString(@"OK", @"OK")]); goto bail; } //check against existing names for(NSString *name in [xpcDaemonClient getProfiles]) { if(NSOrderedSame == [self.profileNameLabel.stringValue caseInsensitiveCompare:name]) { //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Invalid Profile Name", @"Invalid Profile Name"), [NSString stringWithFormat:NSLocalizedString(@"'%@' matches an existing profile name.", @"'%@' matches an existing profile name."), name], @[NSLocalizedString(@"OK", @"OK")]); self.profileNameLabel.stringValue = @""; goto bail; } } //save name self.profileName = self.profileNameLabel.stringValue; //remove current view [self.currentProfileSubview removeFromSuperview]; //update self.currentProfileSubview = self.rulesView; //hide 'show buttons' self.showRulesButton.hidden = YES; //add to rule's view [self.addProfileSheet.contentView addSubview:self.currentProfileSubview]; //update tag self.continueProfileButton.tag = profileRules; break; } //current view: rules // setup next view: modes case profileRules: //remove current view [self.currentProfileSubview removeFromSuperview]; //update self.currentProfileSubview = self.modesView; //add to mode's view [self.addProfileSheet.contentView addSubview:self.currentProfileSubview]; //update tag self.continueProfileButton.tag = profileModes; break; //current view: modes // setup next view: lists case profileModes: //remove current view [self.currentProfileSubview removeFromSuperview]; //update self.currentProfileSubview = self.listsView; //add to list's view [self.addProfileSheet.contentView addSubview:self.currentProfileSubview]; //update tag self.continueProfileButton.tag = profileLists; break; //current view: lists // setup next view: updates case profileLists: //remove current view [self.currentProfileSubview removeFromSuperview]; //update self.currentProfileSubview = self.updateView; //hide button self.updateButton.hidden = YES; //unset label self.updateLabel.stringValue = @""; //add to mode's view [self.addProfileSheet.contentView addSubview:self.currentProfileSubview]; //update tag self.continueProfileButton.tag = profileUpdates; //update button name to "Add Profile" self.continueProfileButton.title = NSLocalizedString(@"Add Profile", @"Add Profile"); break; //current view: updates // add profile as this is the last one! case profileUpdates: //end with 'ok' [self.window endSheet:self.addProfileSheet returnCode:NSModalResponseOK]; default: break; } //uncheck all checks buttons (might be set from current preferences) for (NSView *subview in self.currentProfileSubview.subviews) { if ([subview isKindOfClass:[NSButton class]]) { NSButton *button = (NSButton *)subview; // Uncheck only if the button has a toggleable state if (button.allowsMixedState || button.state != NSControlStateValueOff) { button.state = NSControlStateValueOff; } } } //update view's fame NSRect bounds = self.addProfileSheet.contentView.bounds; NSRect frame = self.currentProfileSubview.frame; frame.origin.x = 0; frame.origin.y = bounds.size.height - frame.size.height; //set frame self.currentProfileSubview.frame = frame; bail: return; } //delete profile button handler -(IBAction)deleteProfile:(id)sender { //response NSModalResponse response = 0; //name NSString* profile = nil; //get profile profile = [self profileFromTable:sender]; //dbg msg os_log_debug(logHandle, "user wants to delete profile '%{public}@'", profile); //show alert response = showAlert(NSAlertStyleInformational, NSLocalizedString(@"Confirm Deletion", @"Confirm Deletion"), [NSString stringWithFormat:NSLocalizedString(@"Delete profile: '%@'?", @"Delete profile: '%@'?"), profile], @[NSLocalizedString(@"Ok", @"Ok"), NSLocalizedString(@"Cancel", @"Cancel")]); //cancel? if(NSAlertSecondButtonReturn == response) { //dbg msg os_log_debug(logHandle, "user canceled deleting profile..."); //done goto bail; } //delete via XPC [xpcDaemonClient deleteProfile:profile]; //dbg msg os_log_debug(logHandle, "deleted profile '%{public}@'", profile); //tell app profiles changed // will grab profile's preferences too [((AppDelegate*)[[NSApplication sharedApplication] delegate]) profilesChanged]; //tell app preferences (maybe) changed [((AppDelegate*)[[NSApplication sharedApplication] delegate]) preferencesChanged:self.preferences]; bail: return; } //'check for update' button handler -(IBAction)check4Update:(id)sender { //update obj Update* update = nil; //disable button self.updateButton.enabled = NO; //reset self.updateLabel.stringValue = @""; //show/start spinner [self.updateIndicator startAnimation:self]; //init update obj update = [[Update alloc] init]; //check // but after a delay for UI dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.75 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ //check for update [update checkForUpdate:^(NSUInteger result, NSString* newVersion) { //process response [self updateResponse:result newVersion:newVersion]; }]; }); return; } //'view rules' button handler // call helper method to show rule's window -(IBAction)viewRules:(id)sender { //call into app delegate to show app rules [((AppDelegate*)[[NSApplication sharedApplication] delegate]) showRules:nil]; return; } //process update response // error, no update, update/new version -(void)updateResponse:(NSInteger)result newVersion:(NSString*)newVersion { //re-enable button self.updateButton.enabled = YES; //stop/hide spinner [self.updateIndicator stopAnimation:self]; switch(result) { //error case Update_Error: //set label self.updateLabel.stringValue = NSLocalizedString(@"error: update check failed", @"error: update check failed"); break; //no updates case Update_None: //dbg msg os_log_debug(logHandle, "no updates available"); //set label self.updateLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Installed version (%@),\r\nis the latest.",@"Installed version (%@),\r\nis the latest."), getAppVersion()]; break; //update is not compatible case Update_NotSupported: //dbg msg os_log_debug(logHandle, "update available, but isn't supported on macOS %ld.%ld", NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, NSProcessInfo.processInfo.operatingSystemVersion.minorVersion); //set label self.updateLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Update available, but isn't supported on macOS %ld.%ld", @"Update available, but isn't supported on macOS %ld.%ld"), NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, NSProcessInfo.processInfo.operatingSystemVersion.minorVersion]; break; //new version case Update_Available: //dbg msg os_log_debug(logHandle, "a new version (%@) is available", newVersion); //alloc update window updateWindowController = [[UpdateWindowController alloc] initWithWindowNibName:@"UpdateWindow"]; //configure [self.updateWindowController configure:[NSString stringWithFormat:NSLocalizedString(@"a new version (%@) is available!",@"a new version (%@) is available!"), newVersion]]; //center window [[self.updateWindowController window] center]; //show it [self.updateWindowController showWindow:self]; //invoke function in background that will make window modal dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //make modal makeModal(self.updateWindowController); }); break; } return; } //button handler // open LuLu home page/docs -(IBAction)openHomePage:(id)sender { //open [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:PRODUCT_URL]]; return; } //on window close // update prefs/set activation policy -(void)windowWillClose:(NSNotification *)notification { //blank allow list? // uncheck 'enabled' and update prefs if(0 == self.allowList.stringValue.length) { //uncheck 'allow list' radio button ((NSButton*)[self.listsView viewWithTag:BUTTON_USE_ALLOW_LIST]).state = NSControlStateValueOff; //disable 'browse' button self.selectAllowListButton.enabled = NSControlStateValueOff; //clear allow list self.allowList.stringValue = @""; //disable allow list input self.allowList.enabled = NSControlStateValueOff; //send XPC msg to daemon to update prefs self.preferences = [xpcDaemonClient updatePreferences:@{PREF_USE_ALLOW_LIST:@0, PREF_ALLOW_LIST:@""}]; } //allow list changed? capture! // this logic is needed, as window can be closed when text field still has focus and 'end edit' won't have fired else if(YES != [self.preferences[PREF_ALLOW_LIST] isEqualToString:self.allowList.stringValue]) { //send XPC msg to daemon to update prefs self.preferences = [xpcDaemonClient updatePreferences:@{PREF_ALLOW_LIST:self.allowList.stringValue}]; } //blank block list? // uncheck 'enabled' and update prefs if(0 == self.blockList.stringValue.length) { //uncheck 'blocklist' radio button ((NSButton*)[self.listsView viewWithTag:BUTTON_USE_BLOCK_LIST]).state = NSControlStateValueOff; //disable 'browse' button self.selectBlockListButton.enabled = NSControlStateValueOff; //clear block list self.blockList.stringValue = @""; //disable block list input self.blockList.enabled = NSControlStateValueOff; //send XPC msg to daemon to update prefs self.preferences = [xpcDaemonClient updatePreferences:@{PREF_USE_BLOCK_LIST:@0, PREF_BLOCK_LIST:@""}]; } //block list changed? capture! // this logic is needed, as window can be closed when text field still has focus and 'end edit' won't have fired else if(YES != [self.preferences[PREF_BLOCK_LIST] isEqualToString:self.blockList.stringValue]) { //send XPC msg to daemon to update prefs // returns (all/latest) prefs, which is what we want self.preferences = [xpcDaemonClient updatePreferences:@{PREF_BLOCK_LIST:self.blockList.stringValue}]; } //wait a bit, then set activation policy dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //on main thread dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //set activation policy [((AppDelegate*)[[NSApplication sharedApplication] delegate]) setActivationPolicy]; }); }); return; } @end ================================================ FILE: LuLu/App/RuleRow.h ================================================ // // file: RuleRow.h // project: lulu (main app) // description: row for 'rules' table (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @interface RuleRow : NSTableRowView @end ================================================ FILE: LuLu/App/RuleRow.m ================================================ // // file: RuleRow.m // project: lulu (main app) // description: row for 'rules' table // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "RuleRow.h" @implementation RuleRow //custom row selection -(void)drawSelectionInRect:(NSRect)dirtyRect { //selection rect NSRect selectionRect = {0}; //selection path NSBezierPath* selectionPath = nil; //selection color NSColor* selectionColor = nil; //highlight selected rows if(self.selectionHighlightStyle != NSTableViewSelectionHighlightStyleNone) { //make selection rect selectionRect = NSInsetRect(self.bounds, 2.5, 2.5); //set color selectionColor = [NSColor unemphasizedSelectedContentBackgroundColor]; //set stroke [selectionColor setStroke]; //set fill [selectionColor setFill]; //create selection path // with rounded corners (5x5) selectionPath = [NSBezierPath bezierPathWithRoundedRect:selectionRect xRadius:5 yRadius:5]; //fill [selectionPath fill]; //stroke [selectionPath stroke]; } return; } @end ================================================ FILE: LuLu/App/RulesMenuController.h ================================================ // // RulesMenuController.h // LuLu // // Created by Patrick Wardle on 1/30/24. // Copyright © 2024 Objective-See. All rights reserved. // #ifndef RulesMenuController_h #define RulesMenuController_h @import Foundation; @interface RulesMenuController : NSObject /* METHODS */ -(void)addRule; -(void)showRules; -(void)exportRules; -(BOOL)importRules; -(NSInteger)cleanupRules; @end #endif /* RulesMenuController_h */ ================================================ FILE: LuLu/App/RulesMenuController.m ================================================ // // RulesMenuController.m // LuLu // // Created by Patrick Wardle on 1/30/24. // Copyright © 2024 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "AppDelegate.h" #import "XPCDaemonClient.h" #import "RulesMenuController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; @implementation RulesMenuController //show rules // call into app delegate to show rules window -(void)showRules { //show rules [((AppDelegate*)[[NSApplication sharedApplication] delegate]) showRules:nil]; return; } //add rule // show rules, then call into app delegate to open add rule window -(void)addRule { //add rules [((AppDelegate*)[[NSApplication sharedApplication] delegate]).rulesWindowController addRule:nil]; return; } //export rules // show panel then write out rules -(void)exportRules { //count __block NSUInteger count = 0; //rules NSDictionary* rules = nil; //error __block NSError* error = nil; //'browse' panel NSSavePanel *panel = nil; //dbg msg os_log_debug(logHandle, "exporting rules..."); //get rules rules = [xpcDaemonClient getRules]; //dbg msg os_log_debug(logHandle, "received %lu rules from daemon", (unsigned long)rules.count); //init panel panel = [NSSavePanel savePanel]; //default to ~/Desktop panel.directoryURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES) firstObject]]; //suggest file name [panel setNameFieldStringValue:NSLocalizedString(@"rules.json", @"rules.json")]; //customize w/ "user only" rules options NSButton *userRulesOnly = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)]; [userRulesOnly setButtonType:NSButtonTypeSwitch]; userRulesOnly.title = NSLocalizedString(@"Only export user created rules", @"Only export user created rules"); userRulesOnly.state = NSControlStateValueOff; [userRulesOnly sizeToFit]; panel.accessoryView = userRulesOnly; //show panel // completion handler will invoked when user clicks 'ok' [panel beginWithCompletionHandler:^(NSInteger result) { //only need to handle 'ok' if(NSModalResponseOK == result) { //only user rules? BOOL exportUserOnly = (NSControlStateValueOn == userRulesOnly.state); //rules (as JSON) NSMutableString* json = [NSMutableString string]; //start [json appendString:@"{"]; //covert each rule (set) for(NSString* key in rules.allKeys) { //flag BOOL hasPersistentRule = NO; //flag BOOL hasUserCreatedRule = NO; //rule set NSDictionary* ruleSet = nil; //extract ruleSet = rules[key]; //for a given item // check all rules for persistent or user created for(Rule* rule in rules[key][KEY_RULES]) { //not temp? if(!hasPersistentRule && ![rule isTemporary]) { hasPersistentRule = YES; } //user created (non temp rule) if(!hasUserCreatedRule && [rule isUserCreated] && ![rule isTemporary]) { hasUserCreatedRule = YES; } } //skip if all item's rules are temp if(YES != hasPersistentRule) { continue; } //when exporting only user rules //skip if all item's rules are *not* user created if(exportUserOnly && !hasUserCreatedRule) { continue; } //add key [json appendFormat:@"\"%@\" : [", key]; //covert each rule for(Rule* rule in rules[key][KEY_RULES]) { //skip temp if(YES == [rule isTemporary]) { continue; } //skip non user created if(exportUserOnly && ![rule isUserCreated]) { continue; } //append rule [json appendFormat:@"{%@},", [rule toJSON]]; //inc count++; } //remove last ',' if(YES == [json hasSuffix:@","]) { //remove [json deleteCharactersInRange:NSMakeRange(json.length-1, 1)]; } //end [json appendString:@"],"]; } //remove last ',' if(YES == [json hasSuffix:@","]) { //remove [json deleteCharactersInRange:NSMakeRange(json.length-1, 1)]; } //end [json appendString:@"}"]; //write out (converted) rules // then activate Finder and select file if(YES == [json writeToURL:panel.URL atomically:NO encoding:NSUTF8StringEncoding error:&error]) { //activate Finder [NSWorkspace.sharedWorkspace selectFile:panel.URL.path inFileViewerRootedAtPath:@""]; } //error else { //err msg os_log_error(logHandle, "ERROR: failed to save rules: %{public}@", error); //show alert showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: Failed to export rules",@"ERROR: Failed to export rules"), NSLocalizedString(@"See log for (more) details",@"See log for (more) details"), @[NSLocalizedString(@"OK", @"OK")]); } } }]; return; } //import rules // show panel, read in rules, parse, send to daemon -(BOOL)importRules { //flag BOOL imported = NO; //flag __block BOOL userOnlyImport = YES; //error NSError* error = nil; //count NSUInteger count = 0; //rules data NSData* data = nil; //rules from disk NSDictionary* importedRules = nil; //rules, as rules NSMutableDictionary* newRules = nil; //archived rules NSData* archivedRules = nil; //'browse' panel NSOpenPanel *panel = nil; //response to 'browse' panel NSInteger response = 0; //init panel panel = [NSOpenPanel openPanel]; //allow files panel.canChooseFiles = YES; //disallow directories panel.canChooseDirectories = NO; //start in ~/Desktop panel.directoryURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES) firstObject]]; //disable multiple selections panel.allowsMultipleSelection = NO; //show it response = [panel runModal]; if(NSModalResponseCancel == response) { //bail goto bail; } //load rules from specified file data = [NSData dataWithContentsOfFile:panel.URL.path options:NSDataReadingMappedIfSafe | NSDataReadingUncached error:&error]; if(nil == data) { //err msg os_log_error(logHandle, "ERROR: failed to load (imported) rules from %{public}@ (error: %{public}@)", panel.URL.path, error); goto bail; } //deserialize @try { //convert importedRules = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; if(nil == importedRules) { //error msg os_log_error(logHandle, "ERROR: failed to convert imported rules from JSON (error: %{public}@)", error); goto bail; } } @catch(NSException* exception) { //bail goto bail; } //sanity check if(YES != [importedRules isKindOfClass:[NSDictionary class]]) { //err msg os_log_error(logHandle, "ERROR: invalid format for imported rules (should be a dictionary, not a %{public}@)", importedRules.className); goto bail; } //init newRules = [NSMutableDictionary dictionary]; //add each for(NSString* key in importedRules) { //sanity check if(YES != [importedRules[key] isKindOfClass:[NSArray class]]) { //err msg os_log_error(logHandle, "ERROR: invalid format for imported rules (should be an array, not a %{public}@)", [importedRules[key] className]); goto bail; } //create rule obj for each for(NSDictionary* importedRule in importedRules[key]) { //rule obj Rule* rule = [[Rule alloc] initFromJSON:importedRule]; //skip if invalid if(nil == rule) { //err msg os_log_error(logHandle, "ERROR: invalid format for imported rule: %{public}@", importedRule); //skip continue; } //found a non-user created rule // means we're going to do a full import if(userOnlyImport && ![rule isUserCreated]) { userOnlyImport = NO; } //inc count++; //first rule // init dictionary, array, etc... if(nil == newRules[key]) { //init newRules[key] = [NSMutableDictionary dictionary]; newRules[key][KEY_RULES] = [NSMutableArray array]; //add cs info if(nil != rule.csInfo) { //add newRules[key][KEY_CS_INFO] = rule.csInfo; } } //add rule obj [newRules[key][KEY_RULES] addObject:rule]; } } //archive rules archivedRules = [NSKeyedArchiver archivedDataWithRootObject:newRules requiringSecureCoding:YES error:&error]; if(nil == archivedRules) { //err msg os_log_error(logHandle, "ERROR: failed to archive rules: %{public}@", error); } //dbg msg os_log_debug(logHandle, "serialized (imported) rules"); //send to daemon if(YES != [xpcDaemonClient importRules:archivedRules userOnly:userOnlyImport]) { //bail goto bail; } //happy imported = YES; //tell (any) windows rules changed [[NSNotificationCenter defaultCenter] postNotificationName:RULES_CHANGED object:nil userInfo:nil]; //show alert showAlert(NSAlertStyleInformational, [NSString stringWithFormat:NSLocalizedString(@"Imported %ld rules",@"Imported %ld rules"), count], nil, @[NSLocalizedString(@"OK", @"OK")]); bail: return imported; } //cleanup rules -(NSInteger)cleanupRules { //result NSInteger cleanedUp = -1; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //first show rules [self showRules]; //show alert // if user cancels, just bail if(NSAlertSecondButtonReturn == showAlert(NSAlertStyleInformational, NSLocalizedString(@"Clean up rules referencing deleted, expired, or terminated items?", @"Clean up rules referencing deleted, expired, or terminated items?"), nil, @[NSLocalizedString(@"OK",@"OK"), NSLocalizedString(@"Cancel",@"Cancel")])) { //dbg msg os_log_debug(logHandle, "user cancelled rule cleanup"); //not an error though cleanedUp = 0; //bail goto bail; } //call into daemon to cleanup // returns number or deleted rules cleanedUp = [xpcDaemonClient cleanupRules:YES]; if(cleanedUp < 0) { //bail goto bail; } //tell (any) windows rules changed [[NSNotificationCenter defaultCenter] postNotificationName:RULES_CHANGED object:nil userInfo:nil]; //share results w/ user showAlert(NSAlertStyleInformational, [NSString stringWithFormat:NSLocalizedString(@"Cleaned up %ld rules",@"Cleaned up %ld rules"), cleanedUp], nil, @[NSLocalizedString(@"OK",@"OK")]); bail: return cleanedUp; } @end ================================================ FILE: LuLu/App/RulesWindowController.h ================================================ // // file: RulesWindowController.h // project: lulu (main app) // description: window controller for 'rules' table (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "Rule.h" #import "XPCDaemonClient.h" #import "AddRuleWindowController.h" #import "ItemPathsWindowController.h" #import "3rd-party/OrderedDictionary.h" /* CONSTS */ //id (tag) for detailed text in category table #define TABLE_ROW_NAME_TAG 100 //id (tag) for detailed text #define TABLE_ROW_SUB_TEXT 101 //id (tag) for delete button #define TABLE_ROW_SELECT_BTN_TAG 100 #define TABLE_ROW_DELETE_BTN_TAG 101 //show path(s) #define MENU_SHOW_PATHS 0 //edit rule(s) #define MENU_EDIT_RULE 1 //delete rule(s) #define MENU_DELETE_RULE 2 /* INTERFACE */ @interface RulesWindowController : NSWindowController /* PROPERTIES */ //(main) outline view @property (weak) IBOutlet NSOutlineView *outlineView; //observer for rules changed @property(nonatomic, retain)id rulesObserver; //loading rules overlay @property (weak) IBOutlet NSVisualEffectView* loadingRules; //loading rules spinner @property (weak) IBOutlet NSProgressIndicator* loadingRulesSpinner; //table items @property(nonatomic, retain)OrderedDictionary* rules; //rules view selector @property (weak) IBOutlet NSPopUpButton *rulesViewSelector; //table items // ...but filtered @property(nonatomic, retain)OrderedDictionary* rulesFiltered; //search box @property (weak) IBOutlet NSSearchField *filterBox; //top level view @property (weak) IBOutlet NSView *view; //window toolbar @property (weak) IBOutlet NSToolbar *toolbar; //selected index in rule view selector @property NSInteger selectedRuleView; //show item path(s) popup contoller @property(strong) ItemPathsWindowController *itemPathsWindowController; //panel for 'add rule' @property (weak) IBOutlet NSView *addRulePanel; //label for add rules button @property (weak) IBOutlet NSTextField *addRuleLabel; //button to add rules @property (weak) IBOutlet NSButton *addRuleButton; //add rules popup controller @property (strong) AddRuleWindowController *addRuleWindowController; //(last) added rule @property(nonatomic,retain)NSString* addedRule; //flag @property BOOL isAscending; /* METHODS */ //configure (UI) -(void)configure; //add a rule -(IBAction)addRule:(id)sender; //delete a rule -(IBAction)deleteRule:(id)sender; @end ================================================ FILE: LuLu/App/RulesWindowController.m ================================================ // // file: RulesWindowController.m // project: lulu (main app) // description: window controller for 'rules' table // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "RuleRow.h" #import "utilities.h" #import "AppDelegate.h" #import "XPCDaemonClient.h" #import "RulesWindowController.h" #import "AddRuleWindowController.h" #import "3rd-party/OrderedDictionary.h" #define SORT_DESCRIPTOR_COLUMN_0 @"sort_0" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; //custom view // to disable highlighting for disabled rules @interface CustomTableCellView : NSTableCellView @property (nonatomic) BOOL isDisabled; @end @implementation CustomTableCellView - (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle { [super setBackgroundStyle:backgroundStyle]; //maintain disabled color even when selected if (self.isDisabled) { self.textField.textColor = NSColor.disabledControlTextColor; [super setBackgroundStyle: NSBackgroundStyleLight]; } } @end @implementation RulesWindowController @synthesize rules; @synthesize toolbar; @synthesize addedRule; @synthesize filterBox; @synthesize addRulePanel; @synthesize loadingRules; @synthesize rulesFiltered; @synthesize rulesObserver; @synthesize loadingRulesSpinner; //init some settings -(void)awakeFromNib { //set target self.outlineView.target = self; //set 2x click handler self.outlineView.doubleAction = @selector(doubleClickHandler:); return; } //configure (UI) -(void)configure { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //set subtitle [self setSubTitle]; //first cleanup any expired/temp rules [xpcDaemonClient cleanupRules:NO]; //then load rules [self loadRules:YES select:@0]; return; } //set subtitle to current profile -(void)setSubTitle { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //current profile NSString* currentProfile = [xpcDaemonClient getCurrentProfile]; //have profile? if(0 != currentProfile.length) { //add subtitle if (@available(macOS 11.0, *)) { self.window.subtitle = [NSString stringWithFormat:NSLocalizedString(@"Current Profile: %@",@"Current Profile: %@"), currentProfile]; } } //set to default else { //set if (@available(macOS 11.0, *)) { self.window.subtitle = NSLocalizedString(@"Current Profile: Default",@"Current Profile: Default"); } } return; } //alloc/init // get rules and listen for new ones -(void)windowDidLoad { //set default rule's view self.selectedRuleView = RULE_TYPE_ALL; //set indentation level for outline view self.outlineView.indentationPerLevel = 42; //pre-req for color of overlay self.loadingRules.wantsLayer = YES; //round overlay's corners self.loadingRules.layer.cornerRadius = 20.0; //mask overlay self.loadingRules.layer.masksToBounds = YES; //set overlay's view material self.loadingRules.material = NSVisualEffectMaterialHUDWindow; //set initial table header self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"All Rules",@"All Rules"); //set sort descriptor for first column [self.outlineView.tableColumns[0] setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:SORT_DESCRIPTOR_COLUMN_0 ascending:YES]]; //set resizing self.outlineView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; //set it to whole too... [self.outlineView setSortDescriptors:@[self.outlineView.tableColumns[0].sortDescriptorPrototype]]; //set flag self.isAscending = YES; //setup observer for new rules // will be broadcast (via XPC) when daemon updates rules self.rulesObserver = [[NSNotificationCenter defaultCenter] addObserverForName:RULES_CHANGED object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { //get new rules [self loadRules:YES select:@(0)]; }]; return; } //get rules from daemon // then, re-load rules table -(void)loadRules:(BOOL)showOverlay select:(NSNumber*)row { //dbg msg os_log_debug(logHandle, "loading rules..."); //show overlay if(YES == showOverlay) { //show overlay self.loadingRules.hidden = NO; //start progress indicator [self.loadingRulesSpinner startAnimation:nil]; } //in background get rules // ...then load rule table table dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //current rules (from ext) NSDictionary* currentRules = nil; //sorted keys NSArray* sortedKeys = nil; //show overlay if(YES == showOverlay) { //nap for UI (loading msg) [NSThread sleepForTimeInterval:0.5f]; } //get rules currentRules = [xpcDaemonClient getRules]; //dbg msg os_log_debug(logHandle, "received %lu rules from daemon: %{public}@", (unsigned long)currentRules.count, currentRules.allKeys); //sync rules @synchronized (self) { //alloc self.rules = [[OrderedDictionary alloc] init]; //dbg msg os_log_debug(logHandle, "sorting rules..."); //sort by (rule) name sortedKeys = [currentRules keysSortedByValueUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { //normal if(YES == self.isAscending) { //compare/return return [((Rule*)[((NSDictionary*)obj1)[KEY_RULES] firstObject]).name compare:((Rule*)[((NSDictionary*)obj2)[KEY_RULES] firstObject]).name options:NSCaseInsensitiveSearch]; } //reversed else { //compare/return return [((Rule*)[((NSDictionary*)obj2)[KEY_RULES] firstObject]).name compare:((Rule*)[((NSDictionary*)obj1)[KEY_RULES] firstObject]).name options:NSCaseInsensitiveSearch]; } }]; //add sorted rules for(NSInteger i = 0; i 0) { selectedRow = MIN(selectedRow, (self.outlineView.numberOfRows-1)); } //dbg msg os_log_debug(logHandle, "reselecting %ld", (long)selectedRow); //reselect [self.outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow] byExtendingSelection:NO]; //scroll [self.outlineView scrollRowToVisible:selectedRow]; } //sync bail: return; } //handler for rule select view -(IBAction)rulesViewSelectorHandler:(id)sender { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //grab tag/save self.selectedRuleView = self.rulesViewSelector.selectedItem.tag; //set column title switch(self.selectedRuleView) { //all case RULE_TYPE_ALL: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"All Rules", @"All Rules"); break; //default case RULE_TYPE_DEFAULT: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"Operating System Programs (required for system functionality)", @"Operating System Programs (required for system functionality)"); break; //apple case RULE_TYPE_APPLE: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"Apple Programs (automatically allowed & added here if 'allow apple programs' is set)", @"Apple Programs (automatically allowed & added here if 'allow apple programs' is set)"); break; //baseline case RULE_TYPE_BASELINE: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"Pre-installed 3rd-party Programs (automatically allowed & added here if 'allow installed applications' is set)", @"Pre-installed 3rd-party Programs (automatically allowed & added here if 'allow installed applications' is set)"); break; //user case RULE_TYPE_USER: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"User-specified Programs (manually added, or in response to an alert)", @"User-specified Programs (manually added, or in response to an alert)"); break; case RULE_TYPE_PASSIVE: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"Added passively (either if 'passive mode' is set, or no user was logged in when rule was created)", @"Added passively (either if 'passive mode' is set, or no user was logged in when rule was created)"); break; case RULE_TYPE_RECENT: self.outlineView.tableColumns.firstObject.headerCell.stringValue = NSLocalizedString(@"Added in last 24 hours (sorted by creation time)", @"Added in last 24 hours (sorted by creation time)"); break; default: break; } //unselect (all) row [self.outlineView deselectAll:nil]; //reload table [self update:@(0)]; //'add rules' only allowed for 'all' and 'user' views if( (self.selectedRuleView == RULE_TYPE_ALL) || (self.selectedRuleView == RULE_TYPE_USER) ) { //change label color to default self.addRuleLabel.textColor = [NSColor labelColor]; //enable button self.addRuleButton.enabled = YES; } //'add rule' not allowed for 'default'/'apple'/'baseline' else { //change label color to gray self.addRuleLabel.textColor = [NSColor controlBackgroundColor]; //disable button self.addRuleButton.enabled = NO; } return; } //filter (search box) handler // just call into update method (which filters, etc) -(IBAction)filterBoxHandler:(id)sender { //dbg msg os_log_debug(logHandle, "filtering rules..."); //update [self update:nil]; return; } //double-click handler -(void)doubleClickHandler:(id)object { NSInteger row = [self.outlineView clickedRow]; if (row < 0) return; //get item id item = [self.outlineView itemAtRow:row]; //only edit if it's a rule (not a group) if ([item isKindOfClass:[NSArray class]]) { return; } //grab rule Rule *rule = (Rule *)item; //dbg msg os_log_debug(logHandle, "editing rule %{public}@", rule); //edit (via add window) [self addRule:rule]; } //warn user the modifying default rules might break things -(NSModalResponse)showDefaultRuleAlert:(Rule*)rule action:(NSString*)action { return showAlert(NSAlertStyleWarning, [NSString stringWithFormat:NSLocalizedString(@"%@ is legitimate macOS process", @"%@ is legitimate macOS process"), rule.name], [NSString stringWithFormat:NSLocalizedString(@"%@ this rule, may impact legitimate system functionalty ...continue?",@"%@ this rule, may impact legitimate system functionalty ...continue?"), action], @[NSLocalizedString(@"Continue", @"Continue"), NSLocalizedString(@"Cancel", @"Cancel")]); } //show paths in sheet -(void)showItemPaths:(NSString*)itemKey { //current rules (from ext) NSDictionary* currentRules = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@", __PRETTY_FUNCTION__, itemKey); //alloc sheet self.itemPathsWindowController = [[ItemPathsWindowController alloc] initWithWindowNibName:@"ItemPaths"]; //get latest rules currentRules = [xpcDaemonClient getRules]; //set rules self.itemPathsWindowController.item = currentRules[itemKey]; //show it [self.window beginSheet:self.itemPathsWindowController.window completionHandler:^(NSModalResponse returnCode) { //unset self.itemPathsWindowController = nil; }]; return; } //button handler for 'add rules' // show 'add rule' sheet and then, on close, add rule via XPC -(IBAction)addRule:(id)sender { //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@", __PRETTY_FUNCTION__, sender); //alloc sheet self.addRuleWindowController = [[AddRuleWindowController alloc] initWithWindowNibName:@"AddRule"]; //invoked with existing rule (to edit) if(YES == [sender isKindOfClass:[Rule class]]) { //default rule? //show alert/warning if(RULE_TYPE_DEFAULT == ((Rule*)sender).type.intValue) { //show alert // ...and bail if user cancels if(NSAlertSecondButtonReturn == [self showDefaultRuleAlert:sender action:@"Editing"]) { //bail goto bail; } } //set rule self.addRuleWindowController.rule = (Rule*)sender; } //show it // on close/OK, invoke XPC to add rule, then reload NSModalResponse response = [NSApp runModalForWindow:self.addRuleWindowController.window]; { //(existing) rule Rule* rule = nil; //dbg msg os_log_debug(logHandle, "add/edit rule window closed..."); //on OK, add rule via XPC if(response == NSModalResponseOK) { //was an update to an existing rule? // delete it first, then go ahead and add if(nil != (rule = self.addRuleWindowController.rule)) { //remove rule via XPC [xpcDaemonClient deleteRule:rule.key rule:rule.uuid]; } //add rule via XPC [xpcDaemonClient addRule:self.addRuleWindowController.info]; //new rule? // save path, and toggle to user tab if(nil == rule) { //save into iVar // allows table to select/scroll to this new rule self.addedRule = self.addRuleWindowController.info[KEY_PATH]; } //reload [self loadRules:YES select:nil]; } //unset add rule window controller self.addRuleWindowController = nil; } bail: return; } //init array of filtered rules // determines what rule view is selected, then sort based on that and also what's in search box -(OrderedDictionary*)filter { //filtered items OrderedDictionary* results = nil; //sorted keys NSArray* sortedKeys = nil; //sorted rules OrderedDictionary* sortedRules = nil; //filter string NSString* filter = nil; //dbg msg os_log_debug(logHandle, "filtering rules..."); //init results = [[OrderedDictionary alloc] init]; //grab filter string filter = self.filterBox.stringValue; //dbg msg os_log_debug(logHandle, "selected rule view: %ld", self.selectedRuleView); //all/no filter // don't need to filter if( (RULE_TYPE_ALL == self.selectedRuleView) && (0 == filter.length) ) { //dbg msg os_log_debug(logHandle, "selected toolbar item is 'all' and filter box is empty ...no need to filter"); //no filter results = self.rules; //bail goto bail; } //dbg msg if(0 != filter.length) { //dbg msg os_log_debug(logHandle, "filtering on '%{public}@'", filter); } //scan all rules // add any that match rule view and filter string {[self.rules enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) { //item // cs info, rules, etc NSMutableDictionary* item = nil; //item's rules NSArray* itemRules = nil; //(item') recent rules NSMutableArray* recentRules = nil; //(item's) rules that match NSMutableArray* matchedRules = nil; //make copy item = [value mutableCopy]; //item's rules itemRules = item[KEY_RULES]; //init matchedRules = [NSMutableArray array]; //not on 'all'/'recent' view? // skip rules if they don't match selected toolbar type if( (RULE_TYPE_ALL != self.selectedRuleView) && (RULE_TYPE_RECENT != self.selectedRuleView) ) { //check all item's rule for match for(Rule* itemRule in itemRules) { //match? if(self.selectedRuleView == itemRule.type.intValue) { //add [matchedRules addObject:itemRule]; } } //done if no item rules match if(0 == matchedRules.count) { return; } //update item[KEY_RULES] = [matchedRules mutableCopy]; //reset // as we reuse this in filtering [matchedRules removeAllObjects]; } //recent? if(RULE_TYPE_RECENT == self.selectedRuleView) { //get (item's) recent rules recentRules = [self recentRules:itemRules]; //item doesn't have any recent rules if(0 == recentRules.count) { //skip return; } //make copy item = [value mutableCopy]; //update item's rules item[KEY_RULES] = recentRules; } //no filter? // we're done if(0 == filter.length) { //append [results insertObject:item forKey:key atIndex:results.count]; //next return; } /* NOW FILTER */ //check each rule(s) on filter string for(Rule* rule in item[KEY_RULES]) { //match? // save rule if(YES == [rule matchesString:filter]) { //add [matchedRules addObject:rule]; } } //any matched (item) rules? // update item rule array and add item if(0 != matchedRules.count) { //update item's rules item[KEY_RULES] = matchedRules; //append to filtered results [results insertObject:item forKey:key atIndex:results.count]; } }];} //sort recent rules if(RULE_TYPE_RECENT == self.selectedRuleView) { //nap for UI (loading msg) [NSThread sleepForTimeInterval:0.5f]; //dbg msg os_log_debug(logHandle, "sorting (recent) rules by creation timestamp..."); //init sortedRules = [[OrderedDictionary alloc] init]; //sort by (rule) timestamp sortedKeys = [results keysSortedByValueUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { //normal if(YES == self.isAscending) { //compare/return return [((Rule*)[((NSDictionary*)obj2)[KEY_RULES] firstObject]).creation compare:((Rule*)[((NSDictionary*)obj1)[KEY_RULES] firstObject]).creation]; } //reversed else { //compare/return return [((Rule*)[((NSDictionary*)obj1)[KEY_RULES] firstObject]).creation compare:((Rule*)[((NSDictionary*)obj2)[KEY_RULES] firstObject]).creation]; } }]; //add sorted rules for(NSInteger i = 0; i *)oldDescriptors { //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //only sort first column: rule name if(YES == [outlineView.sortDescriptors.firstObject.key isEqualToString:SORT_DESCRIPTOR_COLUMN_0]) { //reverse [self.rules reverse]; //toggle self.isAscending = !self.isAscending; //unselect row // want top row to be selected after reverse [self.outlineView deselectAll:nil]; //refresh table [self update:nil]; } return; } //create & customize process cell // these are the root cells, that hold the item (process) -(NSTableCellView*)createProcessCell:(Rule*)rule { //item cell NSTableCellView* processCell = nil; //directory NSString* directory = nil; //create cell processCell = [self.outlineView makeViewWithIdentifier:@"processCell" owner:self]; //global rule? // no icon, no path, etc. if(YES == rule.isGlobal.boolValue) { //set icon processCell.imageView.image = [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode(kGenericHardDiskIcon)]; //set text processCell.textField.stringValue = NSLocalizedString(@"Any program", @"Any program"); //(un)set detailed text ((NSTextField*)[processCell viewWithTag:TABLE_ROW_SUB_TEXT]).stringValue = @""; } //directory rule? else if(YES == rule.isDirectory.boolValue) { //init directory // ...by removing * directory = [rule.path substringToIndex:(rule.path.length-1)]; //set icon processCell.imageView.image = getIconForProcess(directory); //main text // last directory processCell.textField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Programs within \"%@/\"", @"Programs within \"%@/\""), directory.lastPathComponent]; //details // just use path ((NSTextField*)[processCell viewWithTag:TABLE_ROW_SUB_TEXT]).stringValue = rule.path; } //non global rule? // set icon, path, etc. else { //set icon processCell.imageView.image = getIconForProcess(rule.path); //main text // item's name processCell.textField.stringValue = rule.name; //format/set details ((NSTextField*)[processCell viewWithTag:TABLE_ROW_SUB_TEXT]).stringValue = [self formatItemDetails:rule]; } return processCell; } //format details for item -(NSString*)formatItemDetails:(Rule*)rule { //details NSString* details = @""; //cs info? if(nil != rule.csInfo) { //format, based on signer switch([rule.csInfo[KEY_CS_SIGNER] intValue]) { //apple case Apple: details = [NSString stringWithFormat:NSLocalizedString(@"%@ (signer: Apple Proper)", @"%@ (signer: Apple Proper)"), rule.csInfo[KEY_CS_ID]]; break; //app store case AppStore: details = [NSString stringWithFormat:NSLocalizedString(@"%@ (signer: Apple Mac OS App Store)", @"%@ (signer: Apple Mac OS App Store)"), rule.csInfo[KEY_CS_ID]]; break; //dev id case DevID: details = [NSString stringWithFormat:NSLocalizedString(@"%@ (signer: %@)",@"%@ (signer: %@)"), rule.csInfo[KEY_CS_ID], [rule.csInfo[KEY_CS_AUTHS] firstObject]]; break; //ad hoc case AdHoc: details = [NSString stringWithFormat:NSLocalizedString(@"%@ (signer: %@)", @"%@ (signer: %@)"), rule.csInfo[KEY_CS_ID], NSLocalizedString(@"Ad hoc", @"Ad hoc")]; break; default: break; } } //no valid cs info // just use path / and mention issue if(0 == details.length) { //set details = [NSString stringWithFormat:NSLocalizedString(@"%@ (signer: invalid/unsigned)", @"%@ (signer: invalid/unsigned)"), rule.path]; } return details; } //create & customize connection cell -(NSTableCellView*)createConnectionCell:(Rule*)rule { //endpoint port NSString* port = nil; //endpoint addr NSString* address = nil; //item cell //NSTableCellView* cell = nil; //contents NSMutableString* contents = nil; //time stamp NSString* timestamp = nil; //date formatter static NSDateFormatter *dateFormatter = nil; //cell CustomTableCellView *cell = (CustomTableCellView *)[self.outlineView makeViewWithIdentifier:@"simpleCell" owner:self]; //disabled? // set flag (for highlighting) and color cell.isDisabled = rule.isDisabled.boolValue; if (rule.isDisabled.boolValue) { cell.textField.textColor = NSColor.disabledControlTextColor; } else { cell.textField.textColor = NSColor.controlTextColor; } //reset text ((NSTableCellView*)cell).textField.stringValue = @""; ((NSTableCellView*)cell).textField.attributedStringValue = [[NSAttributedString alloc] initWithString:@""]; //set endpoint addr address = (YES == [rule.endpointAddr isEqualToString:VALUE_ANY]) ? NSLocalizedString(@"any address",@"any address") : rule.endpointAddr; //set endpoint port port = (YES == [rule.endpointPort isEqualToString:VALUE_ANY]) ? NSLocalizedString(@"any port",@"any port") : rule.endpointPort; //default contents // address: port contents = [NSMutableString stringWithFormat:@"%@:%@", address, port]; //in "recents" view, add creation timestamp if(RULE_TYPE_RECENT == self.selectedRuleView) { //init dateFormatter = [[NSDateFormatter alloc] init]; //config [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm"]; //init (formatted) timestamp timestamp = [NSString stringWithFormat:@" (created at: %@)", [dateFormatter stringFromDate:rule.creation]]; //append [contents appendString:timestamp]; } //set text cell.textField.stringValue = contents; return cell; } //menu when user 2x clicks, or clicks on rule menu -(IBAction)showRuleMenu:(NSButton*)sender { BOOL mixedStates = NO; NSString* editTitle = nil; NSString* enableTitle = nil; NSString* deleteTitle = nil; NSString* disableTitle = nil; NSString* showPathsTitle = nil; NSInteger row = 0; row = [self.outlineView rowForView:sender]; if (row < 0) return; //get item id item = [self.outlineView itemAtRow:row]; //set flag BOOL isGroup = [item isKindOfClass:[NSArray class]]; //determine if there are mixed states? if(isGroup && [item count] > 1) { BOOL firstState = ((Rule*)[item firstObject]).isDisabled.boolValue; for(int i = 1; i < [item count] && !mixedStates; i++) { mixedStates = (((Rule*)item[i]).isDisabled.boolValue != firstState); } } //set rule Rule* rule = isGroup ? [item firstObject] : (Rule *)item; //set base NSString* base = NSLocalizedString(@"Rule", nil); if(isGroup && [item count] > 1) { base = NSLocalizedString(@"Rules", nil); } //set titles editTitle = [NSString stringWithFormat:NSLocalizedString(@"Edit %@", @"Edit %@"), base]; deleteTitle = [NSString stringWithFormat:NSLocalizedString(@"Delete %@", @"Delete %@"), base]; enableTitle = [NSString stringWithFormat:NSLocalizedString(@"Enable %@", @"Enable %@"), base]; disableTitle = [NSString stringWithFormat:NSLocalizedString(@"Disable %@", @"Disable %@"), base]; //dbg msg os_log_debug(logHandle, "row: %ld, item: %{public}@", (long)row, item); //alloc menu NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Rule Menu"]; menu.autoenablesItems = NO; //start with edit // but only for rules (not group) if(!isGroup) { [menu addItem:[self createMenuItemWithTitle:editTitle action:@selector(editRule:) row:row]]; } //mixed states (in groups) // add both enable and disable if(mixedStates) { [menu addItem:[self createMenuItemWithTitle:enableTitle action:@selector(enableRule:) row:row]]; [menu addItem:[self createMenuItemWithTitle:disableTitle action:@selector(disableRule:) row:row]]; } //not mixed or single rule // so can just add one option to toggle else { //rule disabled? // set 'enable' option if(rule.isDisabled.boolValue) { [menu addItem:[self createMenuItemWithTitle:enableTitle action:@selector(enableRule:) row:row]]; } //rule enabled? // set 'disable' option else { [menu addItem:[self createMenuItemWithTitle:disableTitle action:@selector(disableRule:) row:row]]; } } //separator [menu addItem:[NSMenuItem separatorItem]]; //add delete [menu addItem:[self createMenuItemWithTitle:deleteTitle action:@selector(deleteRule:) row:row]]; //separator [menu addItem:[NSMenuItem separatorItem]]; //add show paths showPathsTitle = NSLocalizedString(@"Display Path(s)", nil); [menu addItem:[self createMenuItemWithTitle:showPathsTitle action:@selector(showPaths:) row:row]]; //show menu below button NSPoint pt = NSMakePoint(NSMinX(sender.bounds), NSMaxY(sender.bounds)); [menu popUpMenuPositioningItem:nil atLocation:pt inView:sender]; return; } //create menu item with title -(NSMenuItem*)createMenuItemWithTitle:(NSString*)title action:(SEL)action row:(NSInteger)row { NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:action keyEquivalent:@""]; item.target = self; item.representedObject = @(row); return item; } //edit rule // just show 'add rule' window with pre-filled data -(void)editRule:(NSMenuItem*)sender { //grab rule // note: edit is only shown for rules (not item group) Rule *rule = [self.outlineView itemAtRow:[sender.representedObject integerValue]]; //dbg msg os_log_debug(logHandle, "editing rule %{public}@", rule); //edit (via add window) [self addRule:rule]; return; } //enable rule(s) via XPC -(void)enableRule:(NSMenuItem*)sender { Rule *rule = nil; NSString* uuid = nil; NSInteger selectedRow = -1; NSInteger row = [sender.representedObject integerValue]; id item = [self.outlineView itemAtRow:row]; if([item isKindOfClass:[NSArray class]]) { rule = [item firstObject]; } else { rule = item; uuid = rule.uuid; } //save selected row selectedRow = self.outlineView.selectedRow; if(-1 == selectedRow) { selectedRow = row; } //dbg msg os_log_debug(logHandle, "enabling rule %{public}@", rule); //enable rule via XPC // nil uuid, means toggle all rules for item (process) [xpcDaemonClient toggleRule:rule.key rule:uuid state:@RULE_TOGGLE_STATE_ENABLE]; //refresh [self loadRules:NO select:@(selectedRow)]; return; } //disable rule(s) via XPC -(void)disableRule:(NSMenuItem*)sender { Rule *rule = nil; NSString* uuid = nil; NSInteger selectedRow = -1; NSInteger row = [sender.representedObject integerValue]; id item = [self.outlineView itemAtRow:row]; if([item isKindOfClass:[NSArray class]]) { rule = [item firstObject]; } else { rule = item; uuid = rule.uuid; } //save selected row selectedRow = self.outlineView.selectedRow; if(-1 == selectedRow) { selectedRow = row; } //dbg msg os_log_debug(logHandle, "disabling rule %{public}@", rule); //disable rule via XPC // nil uuid, means toggle all rules for item (process) [xpcDaemonClient toggleRule:rule.key rule:uuid state:@RULE_TOGGLE_STATE_DISABLE]; //refresh [self loadRules:NO select:@(selectedRow)]; return; } //delete rule(s) via XPC -(void)deleteRule:(NSMenuItem *)sender { Rule *rule = nil; NSString* uuid = nil; NSInteger row = [sender.representedObject integerValue]; id item = [self.outlineView itemAtRow:row]; if([item isKindOfClass:[NSArray class]]) { rule = [item firstObject]; } else { rule = item; uuid = rule.uuid; } //default rule? // show alert/warning if(RULE_TYPE_DEFAULT == rule.type.intValue) { //show alert // ...and bail if user cancels if(NSAlertSecondButtonReturn == [self showDefaultRuleAlert:rule action:@"Deleting"]) { //bail goto bail; } } //dbg msg os_log_debug(logHandle, "deleting rule key: %{public}@, rule uuid: %{public}@", rule.key, uuid); //remove rule via XPC // nil uuid, means delete all rules for item (process) [xpcDaemonClient deleteRule:rule.key rule:uuid]; //refresh [self loadRules:NO select:@(row)]; bail: return; } //show paths -(void)showPaths:(NSMenuItem *)sender { Rule *rule = nil; NSInteger row = [sender.representedObject integerValue]; id item = [self.outlineView itemAtRow:row]; if([item isKindOfClass:[NSArray class]]) { rule = [item firstObject]; } else { rule = item; } [self showItemPaths:rule.key]; return; } //find row for item -(NSInteger)findRowForItem:(id)item { //row NSInteger row = -1; //current item id currentItem = nil; //scan outline to find matching object for(NSUInteger i = 0; i < self.outlineView.numberOfRows; i++) { //extract current item currentItem = [self.outlineView itemAtRow:i]; //looking for path? // only apply to item/process objects if( (YES == [item isKindOfClass:[NSString class]]) && (YES == [currentItem isKindOfClass:[NSArray class]]) ) { //paths match? if(YES == [item isEqualToString:((Rule*)[currentItem firstObject]).path]) { //save index row = i; //all done break; } } //looking for item? // grab first rule from it's array and compare paths else if( (YES == [item isKindOfClass:[NSArray class]]) && (YES == [currentItem isKindOfClass:[NSArray class]]) ) { //paths match? if(YES == [((Rule*)[item firstObject]).path isEqualToString:((Rule*)[currentItem firstObject]).path]) { //save index row = i; //all done break; } } //looking for rule? else if( (YES == [item isKindOfClass:[Rule class]]) && (YES == [currentItem isKindOfClass:[Rule class]]) ) { //rules match? if(YES == [(Rule*)item isEqualToRule:(Rule*)currentItem]) { //save index row = i; //all done break; } } }//all items return row; } //button handler // open LuLu home page/docs -(IBAction)openHomePage:(id)sender { //open [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:PRODUCT_URL]]; return; } //on window close // set activation policy -(void)windowWillClose:(NSNotification *)notification { //cleanup any expired/temp rules [xpcDaemonClient cleanupRules:NO]; //wait a bit, then set activation policy dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //on main thread dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //set activation policy [((AppDelegate*)[[NSApplication sharedApplication] delegate]) setActivationPolicy]; }); }); return; } @end ================================================ FILE: LuLu/App/SigningInfoViewController.h ================================================ // // file: SigningInfoViewController // project: lulu (login item) // description: view controller for signing info popup (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // @import Cocoa; /* DEFINES */ //signing auths view #define SIGNING_AUTH_1 1 @interface SigningInfoViewController : NSViewController { } /* METHODS */ /* PROPERTIES */ //alert info @property(nonatomic, retain)NSDictionary* alert; //signing icon @property (weak) IBOutlet NSImageView* icon; //main signing msg @property (weak) IBOutlet NSTextField* message; //details @property (weak) IBOutlet NSTextField* details; @end ================================================ FILE: LuLu/App/SigningInfoViewController.m ================================================ // // file: SigningInfoViewController // project: lulu (login item) // description: view controller for signing info popup (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "SigningInfoViewController.h" @implementation SigningInfoViewController @synthesize alert; //automatically invoked -(void)popoverWillShow:(NSNotification *)notification; { //signing info NSDictionary* signingInfo = nil; //summary NSMutableString* summary = nil; //signing ID // default to blank NSString* signingID = @""; //alloc string for summary summary = [NSMutableString string]; //extract signing info signingInfo = alert[KEY_CS_INFO]; //start summary with item name [summary appendString:alert[KEY_PROCESS_NAME]]; //unset signing auth field for(NSUInteger i=0; i<3; i++) { //unset ((NSTextField*)[self.view viewWithTag:SIGNING_AUTH_1+i]).stringValue = @""; } //no signing info? if(nil == signingInfo) { //append to summary [summary appendFormat:NSLocalizedString(@" is not validly signed", @" is not validly signed")]; //details: n/a self.details.stringValue = NSLocalizedString(@"not applicable", @"not applicable"); //bail goto bail; } //process switch([signingInfo[KEY_CS_STATUS] integerValue]) { //happily signed case noErr: //append to summary [summary appendFormat:NSLocalizedString(@" is validly signed", @" is validly signed")]; //set signing id if(nil != signingInfo[KEY_CS_ID]) { //set signingID = signingInfo[KEY_CS_ID]; } //item signed by apple if(Apple == [signingInfo[KEY_CS_SIGNER] intValue]) { //set details self.details.stringValue = [NSString stringWithFormat:NSLocalizedString(@"%@ signed by Apple proper", @"%@ signed by Apple proper"), signingID]; } //item signed, third party/ad hoc, etc else { //from app store? if(AppStore == [signingInfo[KEY_CS_SIGNER] intValue]) { //set details self.details.stringValue = [NSString stringWithFormat:NSLocalizedString(@"%@ signed by Mac App Store", @"%@ signed by Mac App Store"), signingID]; } //developer id? else if(DevID == [signingInfo[KEY_CS_SIGNER] intValue]) { //set details self.details.stringValue = [NSString stringWithFormat:NSLocalizedString(@"%@ signed with an Apple Developer ID", @"%@ signed with an Apple Developer ID"), signingID]; } //something else // ad hoc? 3rd-party? else if(AdHoc == [signingInfo[KEY_CS_SIGNER] intValue]) { //set details self.details.stringValue = [NSString stringWithFormat:NSLocalizedString(@"%@ signed by 3rd-party/ad hoc", @"%@ signed by 3rd-party/ad hoc"), signingID]; } else { //set details self.details.stringValue = NSLocalizedString(@" unknown", @" unknown"); } } //no signing auths // usually (always?) adhoc if(0 == [signingInfo[KEY_CS_AUTHS] count]) { //set signing auths field to none ((NSTextField*)[self.view viewWithTag:SIGNING_AUTH_1]).stringValue = NSLocalizedString(@"› no signing authorities", @"› no signing authorities"); } //add each signing auth // should one be max of three else { //add signing auth for(NSUInteger i=0; i<[signingInfo[KEY_CS_AUTHS] count]; i++) { //exit loop at three if(i == 3) break; //add ((NSTextField*)[self.view viewWithTag:SIGNING_AUTH_1+i]).stringValue = [NSString stringWithFormat:@"› %@ \n", signingInfo[KEY_CS_AUTHS][i]]; } } break; //unsigned case errSecCSUnsigned: //append to summary [summary appendFormat:NSLocalizedString(@" is not signed", @" is not signed")]; //details: n/a self.details.stringValue = NSLocalizedString(@"not applicable", @"not applicable"); //set signing auths field to n/a ((NSTextField*)[self.view viewWithTag:SIGNING_AUTH_1]).stringValue = NSLocalizedString(@"not applicable", @"not applicable"); break; //everything else // other signing errors default: //append to summary [summary appendFormat:NSLocalizedString(@" has a signing issue", @" has a signing issue")]; //set details self.details.stringValue = [NSMutableString stringWithFormat:NSLocalizedString(@"%@ signing error: %#lx", @"%@ signing error: %#lx"), signingID, (long)[signingInfo[KEY_CS_STATUS] integerValue]]; //set signing auths field to n/a ((NSTextField*)[self.view viewWithTag:SIGNING_AUTH_1]).stringValue = NSLocalizedString(@"not applicable", @"not applicable"); break; } bail: //assign summary to outlet self.message.stringValue = summary; return; } @end ================================================ FILE: LuLu/App/StartupWindowController.h ================================================ // // file: StartupWindowController.h // // created by Patrick Wardle // copyright (c) 2024 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; @interface StartupWindowController : NSWindowController { } //version warning msg @property (weak) IBOutlet NSTextField *versionWarning; //activity indicator @property (weak) IBOutlet NSProgressIndicator *spinner; @end ================================================ FILE: LuLu/App/StartupWindowController.m ================================================ // // file: StartupWindowController.m // // created by Patrick Wardle // copyright (c) 2024 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "AppDelegate.h" #import "StartupWindowController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation StartupWindowController @synthesize spinner; //automatically called when nib is loaded -(void)awakeFromNib { //center [self.window center]; //start progress indicator [self.spinner startAnimation:nil]; //not in dark mode? // make window white if(YES != isDarkMode()) { //make white self.window.backgroundColor = NSColor.whiteColor; } //set transparency self.window.titlebarAppearsTransparent = YES; //need to display warning? [self displayVersionWarning]; //make it key window [self.window makeKeyAndOrderFront:self]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //(re)make front [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; return; } //show version warning if on macOS 15, but less than 15.3 -(void)displayVersionWarning { //get OS version NSOperatingSystemVersion version = NSProcessInfo.processInfo.operatingSystemVersion; //default hide self.versionWarning.hidden = YES; //macOS 15 // but less than 15.3? ...warn if(version.majorVersion == 15 && version.minorVersion < 3) { //show warning self.versionWarning.hidden = NO; } return; } @end ================================================ FILE: LuLu/App/StatusBarItem.h ================================================ // // file: StatusBarMenu.h // project: lulu (login item) // description: menu handler for status bar icon (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "RulesMenuController.h" @interface StatusBarItem : NSObject { } //status item @property(nonatomic, strong, readwrite)NSStatusItem* statusItem; //rules (sub)menu handler @property(nonatomic, retain)RulesMenuController* rulesMenuController; //popover @property(retain, nonatomic)NSPopover* popover; //disabled flag @property BOOL isDisabled; /* METHODS */ //remove status item -(void)removeStatusItem; //init -(id)init:(NSMenu*)menu preferences:(NSDictionary*)preferences; //set to current profile -(void)setProfile; @end ================================================ FILE: LuLu/App/StatusBarItem.m ================================================ // // file: StatusBarMenu.m // project: lulu (login item) // description: menu handler for status bar icon // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "Extension.h" #import "AppDelegate.h" #import "StatusBarItem.h" #import "StatusBarPopoverController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //xpc for daemon comms extern XPCDaemonClient* xpcDaemonClient; //menu items enum menuItems { status = 100, profile, toggle, rulesShow, rulesAdd, rulesExport, rulesImport, rulesCleanup, profilesManage, prefs, monitor, quit, uninstall, support, end }; @implementation StatusBarItem @synthesize isDisabled; @synthesize statusItem; @synthesize rulesMenuController; //init method // set some intial flags, init daemon comms, etc. -(id)init:(NSMenu*)menu preferences:(NSDictionary*)preferences { //token static dispatch_once_t onceToken = 0; //super self = [super init]; if(self != nil) { //init rules (sub)menu handler rulesMenuController = [[RulesMenuController alloc] init]; //create item [self createStatusItem:menu]; //set state based on (existing) preferences self.isDisabled = [preferences[PREF_IS_DISABLED] boolValue]; //only once // show popover dispatch_once(&onceToken, ^{ //parent NSDictionary* parent = nil; //get real parent parent = getRealParent(getpid()); //dbg msg os_log_debug(logHandle, "(real) parent: %{public}@", parent); //only show popover if we're not autolaunched if(YES != [parent[@"CFBundleIdentifier"] isEqualToString:@"com.apple.loginwindow"]) { //dbg msg os_log_debug(logHandle, "...user launched, so will show status bar popover"); //show [self showPopover]; } }); //set initial menu state [self setState]; } return self; } //create status item -(void)createStatusItem:(NSMenu*)menu { //init status item statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; //set menu / delegate self.statusItem.menu = menu; self.statusItem.menu.delegate = self; //set handler for each menu item [self setMenuHandler:menu]; //disable first two menu items // as they are purely informative [menu.itemArray[0] setEnabled:NO]; [menu.itemArray[0] setAction:nil]; [menu.itemArray[1] setEnabled:NO]; [menu.itemArray[1] setAction:nil]; //init profiles items/sub-menu [self setProfile]; return; } //menu 'will open' delegate // make sure popover is closed -(void)menuWillOpen:(NSMenu *)menu { //make sure to close popover first if(YES == self.popover.shown) { //close [self.popover performClose:nil]; } return; } //set handler for menu item(s) -(void)setMenuHandler:(NSMenu*)menu { //iterate over all menu items // add target, enable, and handler for each for(NSMenuItem* menuItem in menu.itemArray) { //handle sub-menu(s) if(nil != menuItem.submenu) { //recursively set actions for submenu items [self setMenuHandler:menuItem.submenu]; continue; } //set target menuItem.target = self; //enable menuItem.enabled = YES; //set action, to handler menuItem.action = @selector(handler:); } return; } //remove status item -(void)removeStatusItem { //remove item [[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem]; //unset self.statusItem = nil; return; } //show popver -(void)showPopover { //alloc popover self.popover = [[NSPopover alloc] init]; //don't want highlight for popover self.statusItem.button.cell.highlighted = NO; //set target self.statusItem.button.target = self; //set view controller self.popover.contentViewController = [[StatusBarPopoverController alloc] initWithNibName:@"StatusBarPopover" bundle:nil]; //set behavior // don't want it close before timeout (unless user clicks '^') self.popover.behavior = NSPopoverBehaviorApplicationDefined; //set delegate self.popover.delegate = self; //show popover // have to wait cuz... dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ //show [self.popover showRelativeToRect:self.statusItem.button.bounds ofView:self.statusItem.button preferredEdge:NSMinYEdge]; }); //wait a bit // then automatically hide popup if user has not closed it dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ //still visible? // close it then... if(YES == self.popover.shown) { //close [self.popover performClose:nil]; } //remove action handler self.statusItem.button.action = nil; //reset highlight mode ((NSButtonCell*)self.statusItem.button.cell).highlightsBy = NSContentsCellMask | NSChangeBackgroundCellMask; }); return; } //cleanup popover -(void)popoverDidClose:(NSNotification *)notification { //unset self.popover = nil; //reset highlight mode ((NSButtonCell*)self.statusItem.button.cell).highlightsBy = NSContentsCellMask | NSChangeBackgroundCellMask; return; } //menu handler -(void)handler:(id)sender { //dbg msg os_log_debug(logHandle, "handling button click: %{public}@ (%ld)", ((NSButton*)sender).title, ((NSButton*)sender).tag); //handle user selection switch(((NSMenuItem*)sender).tag) { //toggle case toggle: { //dbg msg os_log_debug(logHandle, "toggling (%d -> %d)", self.isDisabled, !self.isDisabled); //invert since toggling self.isDisabled = !self.isDisabled; //set menu state [self setState]; //update prefs [xpcDaemonClient updatePreferences:@{PREF_IS_DISABLED:[NSNumber numberWithBool:self.isDisabled]}]; //toggle network extension based on (new) state dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //toggle [[[Extension alloc] init] toggleNetworkExtension:!self.isDisabled]; }); break; } //rules: show case rulesShow: //show [self.rulesMenuController showRules]; break; //rules: add case rulesAdd: //show first [self.rulesMenuController showRules]; //add [self.rulesMenuController addRule]; break; //rules: export case rulesExport: //show first [self.rulesMenuController showRules]; //export [self.rulesMenuController exportRules]; break; //rules: import case rulesImport: //import if(YES != [self.rulesMenuController importRules]) { //show alert showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: Failed to import rules", @"ERROR: Failed to import rules"), NSLocalizedString(@"See log for (more) details",@"See log for (more) details"), @[NSLocalizedString(@"OK", @"OK")]); //bail goto bail; } //then show rules [self.rulesMenuController showRules]; break; //rules: cleanup case rulesCleanup: //cleanup if([self.rulesMenuController cleanupRules] < 0) { //show alert showAlert(NSAlertStyleWarning, NSLocalizedString(@"ERROR: Failed to cleanup rules", @"ERROR: Failed to cleanup rules"), NSLocalizedString(@"See log for (more) details",@"See log for (more) details"), @[NSLocalizedString(@"OK",@"OK")]); //bail goto bail; } break; //profiles case profilesManage: [((AppDelegate*)[[NSApplication sharedApplication] delegate]) showPreferences:TOOLBAR_PROFILES_ID]; break; //prefs // default to rules case prefs: [((AppDelegate*)[[NSApplication sharedApplication] delegate]) showPreferences:TOOLBAR_RULES_ID]; break; //monitor // launch netiquette (with lulu args) case monitor: { //path NSURL* path = nil; //error NSError* error = nil; //init path path = [NSURL fileURLWithPath:[NSBundle.mainBundle.resourcePath stringByAppendingPathComponent:NETWORK_MONITOR]]; //dbg msg os_log_debug(logHandle, "launching network monitor (%{public}@)", path); //launch // with args if(nil == [NSWorkspace.sharedWorkspace launchApplicationAtURL:path options:0 configuration:[NSDictionary dictionaryWithObject:@[@"-lulu"] forKey:NSWorkspaceLaunchConfigurationArguments] error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to launch network monitor, %{public}@, (error: %{public}@)", path, error); } break; } //quit case quit: [((AppDelegate*)[[NSApplication sharedApplication] delegate]) quit:sender]; break; //uninstall case uninstall: [((AppDelegate*)[[NSApplication sharedApplication] delegate]) uninstall:sender]; break; //support case support: //open URL // invokes user's default browser [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:PATREON_URL]]; break; default: break; } bail: return; } //set current profile -(void)setProfile { //current NSString* current = [xpcDaemonClient getCurrentProfile]; //profiles NSMutableArray* profiles = [xpcDaemonClient getProfiles]; //grab menu NSMenu* menu = [((AppDelegate*)[[NSApplication sharedApplication] delegate]) profilesMenu]; //set current profile if(nil != current) { //set [self.statusItem.menu itemWithTag:profile].title = [NSString stringWithFormat:NSLocalizedString(@"Profile: %@", @"Profile: %@"), current]; } //otherwise (re)set to default else { //set [self.statusItem.menu itemWithTag:profile].title = NSLocalizedString(@"Profile: Default", @"Profile: Default"); } //reset profiles menu [menu removeAllItems]; //have profiles? // add each name and enable 'Switch' menu item if(0 != profiles.count) { //enable [[((AppDelegate*)[[NSApplication sharedApplication] delegate]) profileSwitchMenuItem] setEnabled:YES]; //add default first/top [profiles insertObject:NSLocalizedString(@"Default", @"Default") atIndex:0]; //add each name for(NSString *name in profiles) { //menu item NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:name action:@selector(switchToProfile:) keyEquivalent:@""]; //target item.target = self; //default to off item.state = NSControlStateValueOff; //name // though keep default 'nil' if(NSOrderedSame != [name caseInsensitiveCompare:NSLocalizedString(@"Default", @"Default")]) { //set item.representedObject = name; } //add [menu addItem:item]; //should select 'default' as current? if( (nil == current) && (NSOrderedSame == [name caseInsensitiveCompare:NSLocalizedString(@"Default", @"Default")]) ) { //set item.state = NSControlStateValueOn; } //should select other as current? else if(YES == [item.title isEqualToString:current]) { //set item.state = NSControlStateValueOn; } } } //otherwise disable else { //disable [[((AppDelegate*)[[NSApplication sharedApplication] delegate]) profileSwitchMenuItem] setEnabled:NO]; } return; } //switch profile - (void)switchToProfile:(NSMenuItem *)sender { //grab profile NSString* profile = sender.representedObject; //dbg msg os_log_debug(logHandle, "user wants to change profile to '%{public}@'", profile ? profile : @"Default"); //set profile via XPC // nil is ok, means (re)set to default [xpcDaemonClient setProfile:profile]; //tell app profiles changed // will also update status menu [((AppDelegate*)[[NSApplication sharedApplication] delegate]) profilesChanged]; //tell app preferences changed [((AppDelegate*)[[NSApplication sharedApplication] delegate]) preferencesChanged:[xpcDaemonClient getPreferences]]; //show alert showAlert(NSAlertStyleInformational, NSLocalizedString(@"Profile Switched", @"Profile Switched"), [NSString stringWithFormat:NSLocalizedString(@"Current profile is now: '%@'.", @"Current profile is now: '%@'."), nil != profile ? profile : NSLocalizedString(@"Default", @"Default")], @[NSLocalizedString(@"OK", @"OK")]); return; } //set menu status // logic based on 'isEnabled' iVar -(void)setState { //dbg msg os_log_debug(logHandle, "setting state to: %@", (self.isDisabled) ? @"disabled" : @"enabled"); //set to disabled if(YES == self.isDisabled) { //update status [self.statusItem.menu itemWithTag:status].title = NSLocalizedString(@"LuLu: disabled", @"LuLu: disabled"); //set icon self.statusItem.button.image = [NSImage imageNamed:@"StatusInactive"]; //change toggle text [self.statusItem.menu itemWithTag:toggle].title = NSLocalizedString(@"Enable", @"Enable"); } //set to enabled else { //update status [self.statusItem.menu itemWithTag:status].title = NSLocalizedString(@"LuLu: enabled", @"LuLu: enabled"); //set icon self.statusItem.button.image = [NSImage imageNamed:@"StatusActive"]; //change toggle text [self.statusItem.menu itemWithTag:toggle].title = NSLocalizedString(@"Disable", @"Disable"); } return; } @end ================================================ FILE: LuLu/App/StatusBarPopoverController.h ================================================ // // file: StatusBarPopoverController.h // project: lulu (login item) // description: popover for status bar (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Cocoa; @interface StatusBarPopoverController : NSViewController @end ================================================ FILE: LuLu/App/StatusBarPopoverController.m ================================================ // // file: StatusBarPopoverController.m // project: lulu (login item) // description: popover for status bar // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "AppDelegate.h" #import "StatusBarPopoverController.h" @implementation StatusBarPopoverController //'close' button handler // simply dismiss/close popover -(IBAction)closePopover:(NSControl *)sender { //close [[[self view] window] close]; return; } @end ================================================ FILE: LuLu/App/Update.h ================================================ // // file: Update.h // project: lulu (shared) // description: checks for new versions of LuLu (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #ifndef Update_h #define Update_h @import Cocoa; @import Foundation; @interface Update : NSObject //check for an update // will invoke app delegate method to update UI when check completes -(void)checkForUpdate:(void (^)(NSUInteger result, NSString* latestVersion))completionHandler; @end #endif /* Update_h */ ================================================ FILE: LuLu/App/Update.m ================================================ // // file: Update.m // project: lulu (shared) // description: checks for new versions of LuLu // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "Update.h" #import "utilities.h" #import "AppDelegate.h" /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation Update //check for an update // will invoke app delegate method to update UI when check completes -(void)checkForUpdate:(void (^)(NSUInteger result, NSString* latestVersion))completionHandler { //get latest version in background dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //result NSInteger result = Update_None; //product info NSDictionary* productInfo = nil; //latest version NSString* latestVersion = nil; //major/minor NSNumber* osMajor = nil; NSNumber* osMinor = nil; //get product info productInfo = [self getProductInfo:PRODUCT_NAME]; if(nil != productInfo) { //first check if there is a new version latestVersion = productInfo[LATEST_VERSION]; if(YES == [latestVersion isKindOfClass:[NSString class]]) { //trim latestVersion = [latestVersion stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; //check current version with latest if(NSOrderedAscending == [getAppVersion() compare:latestVersion options:NSNumericSearch]) { //update available result = Update_Available; } } //then check if new version is compatible w/ user version of macOS if(result == Update_Available) { osMajor = productInfo[SUPPORTED_OS_MAJOR]; osMinor = productInfo[SUPPORTED_OS_MINOR]; if( (YES == [osMajor isKindOfClass:[NSNumber class]]) && (YES == [osMinor isKindOfClass:[NSNumber class]]) ) { //init NSOperatingSystemVersion supportedOS = { .majorVersion = osMajor.intValue, .minorVersion = osMinor.intValue, .patchVersion = 0 }; //user's version of macOS supported? if(YES != [NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:supportedOS]) { //dbg mdg os_log_debug(logHandle, "Latest version requires macOS %ld.%ld, but current macOS is %{public}@", supportedOS.majorVersion, supportedOS.minorVersion, NSProcessInfo.processInfo.operatingSystemVersionString); //not supported result = Update_NotSupported; } } } } //error else { //err msg os_log_error(logHandle, "ERROR: Failed to retrieve product info (for update check) from %{public}@", PRODUCT_VERSIONS_URL); result = Update_Error; } //invoke app delegate method // will update UI/show popup if necessary dispatch_async(dispatch_get_main_queue(), ^{ completionHandler(result, latestVersion); }); }); return; } //read JSON file w/ products // return dictionary w/ info about this product -(NSDictionary*)getProductInfo:(NSString*)product { //product version(s) data NSData* json = nil; NSError* error = nil; NSDictionary* products = nil; NSDictionary* productInfo = nil; //get json file (products) from remote URL @try { //get JSON json = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:PRODUCT_VERSIONS_URL]]; if(nil == json) { //err msg os_log_error(logHandle, "ERROR: failed to download product info from %{public}@", PRODUCT_VERSIONS_URL); goto bail; } //convert products = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error]; if(nil != error) { //err msg os_log_error(logHandle, "ERROR: Failed to convert 'products' to JSON (error: %{public}@)", error); goto bail; } //extract product info if( (YES == [products isKindOfClass:[NSDictionary class]]) && (YES == [products[product] isKindOfClass:[NSDictionary class]]) ) { productInfo = products[product]; } } @catch(NSException* exception) { //err msg os_log_error(logHandle, "ERROR: Failed to convert 'products' to JSON (exception: %{public}@)", exception); } bail: return productInfo; } @end ================================================ FILE: LuLu/App/UpdateWindowController.h ================================================ // // file: UpdateWindowController.m // project: lulu // description: window handler for update window/popup (header) // // created by Patrick Wardle // copyright (c) 2020 Objective-See. All rights reserved. // @import Cocoa; @interface UpdateWindowController : NSWindowController { } /* PROPERTIES */ //version label/string @property(weak)IBOutlet NSTextField *infoLabel; //action button @property(weak)IBOutlet NSButton *actionButton; //label string @property(nonatomic, retain)NSString* infoLabelString; /* METHODS */ //save the main label -(void)configure:(NSString*)label; //invoked when user clicks button // ->trigger action such as opening product website, updating, etc -(IBAction)buttonHandler:(id)sender; @end ================================================ FILE: LuLu/App/UpdateWindowController.m ================================================ // // file: UpdateWindowController.m // project: lulu // description: window handler for update window/popup // // created by Patrick Wardle // copyright (c) 2020 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "AppDelegate.h" #import "UpdateWindowController.h" @implementation UpdateWindowController @synthesize infoLabel; @synthesize actionButton; @synthesize infoLabelString; //automatically called when nib is loaded -(void)awakeFromNib { //center [self.window center]; return; } //automatically invoked when window is loaded -(void)windowDidLoad { //super [super windowDidLoad]; //not in dark mode? // make window white if(YES != isDarkMode()) { //make white self.window.backgroundColor = NSColor.whiteColor; } //indicated title bar is transparent (too) self.window.titlebarAppearsTransparent = YES; //set main label self.infoLabel.stringValue = self.infoLabelString; //make button first responder // calling this without a timeout sometimes fails :/ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ //make first responder [self.window makeFirstResponder:self.actionButton]; }); //make it key window [self.window makeKeyAndOrderFront:self]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //(re)make front [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; return; } //automatically invoked when window is closing // ->make ourselves unmodal -(void)windowWillClose:(NSNotification *)notification { //make un-modal [[NSApplication sharedApplication] stopModal]; return; } //save the main label -(void)configure:(NSString*)label { //save label's string self.infoLabelString = label; return; } //invoked when user clicks button -(IBAction)buttonHandler:(id)sender { //open URL [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:PRODUCT_URL]]; //always close window [[self window] close]; return; } @end ================================================ FILE: LuLu/App/WelcomeWindowController.h ================================================ // // LinkWindowController.h // LuLu // // Created by Patrick Wardle on 1/25/18. // Copyright (c) 2018 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; #import "XPCDaemonClient.h" @interface WelcomeWindowController : NSWindowController /* PROPERTIES */ //main view controller @property(nonatomic, retain)NSViewController* welcomeViewController; //welcome view @property (strong) IBOutlet NSView *welcomeView; //allow extension view @property (strong) IBOutlet NSView *allowExtensionView; //allow extension spinner @property (weak) IBOutlet NSProgressIndicator *allowExtActivityIndicator; //allow extension message @property (weak) IBOutlet NSTextField *allowExtMessage; //approve extension image @property (weak) IBOutlet NSImageView *approveExt; //approve extension message @property (weak) IBOutlet NSTextField *approveExtMessage; //config view @property (strong) IBOutlet NSView *configureView; //allow apple bins/apps @property (weak) IBOutlet NSButton *allowApple; //allow 3rd-party installed apps @property (weak) IBOutlet NSButton *allowInstalled; //allow dns traffic installed apps @property (weak) IBOutlet NSButton *allowDNS; //support view @property (strong) IBOutlet NSView *supportView; //preferences @property (nonatomic, retain)NSDictionary* preferences; /* METHODS */ //show a view // note: replaces old view and highlights specified responder -(void)showView:(NSView*)view firstResponder:(NSInteger)firstResponder; @end ================================================ FILE: LuLu/App/WelcomeWindowController.m ================================================ // // file: WelcomeWindowController.m // project: lulu (main app) // description: menu handler for status bar icon // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "Extension.h" #import "AppDelegate.h" #import "XPCDaemonClient.h" #import "WelcomeWindowController.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //buttons #define SHOW_WELCOME 0 #define SHOW_ALLOW_EXT 1 #define SHOW_CONFIGURE 2 #define SHOW_SUPPORT 3 #define SUPPORT_NO 4 #define SUPPORT_YES 5 @implementation WelcomeWindowController @synthesize preferences; @synthesize welcomeViewController; //welcome! -(void)windowDidLoad { //super [super windowDidLoad]; //not in dark mode? // make window white if(YES != isDarkMode()) { //make white self.window.backgroundColor = NSColor.whiteColor; } //when supported // indicate title bar is transparent (too) if(YES == [self.window respondsToSelector:@selector(titlebarAppearsTransparent)]) { //set transparency self.window.titlebarAppearsTransparent = YES; } //set title self.window.title = [NSString stringWithFormat:@"LuLu v%@", getAppVersion()]; //show welcome view [self showView:self.welcomeView firstResponder:SHOW_ALLOW_EXT]; //make key and front [self.window makeKeyAndOrderFront:self]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //(re)make front [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; return; } //button handler for all views // show next view, sometimes, with view specific logic -(IBAction)buttonHandler:(id)sender { //leaving prefs view? // capture prefs/set defaults if( (SHOW_CONFIGURE+1) == ((NSToolbarItem*)sender).tag) { //capture self.preferences = @{PREF_ALLOW_APPLE:[NSNumber numberWithBool:self.allowApple.state], PREF_ALLOW_INSTALLED: [NSNumber numberWithBool:self.allowInstalled.state], PREF_ALLOW_DNS: [NSNumber numberWithBool:self.allowDNS.state], PREF_ALLOW_LOCALHOST:@YES, PREF_ALLOW_SIMULATOR:@NO, PREF_PASSIVE_MODE:@NO, PREF_PASSIVE_MODE_ACTION:@0, PREF_BLOCK_MODE:@NO, PREF_NO_ICON_MODE:@NO, PREF_NO_VT_MODE:@NO, PREF_NO_UPDATE_MODE:@NO, PREF_INSTALL_TIMESTAMP:[NSDate date]}; } //set next view switch(((NSButton*)sender).tag) { //show "allow extension" view // waits until extension is loaded! case SHOW_ALLOW_EXT: { //skip if extension is already active if(YES == [[[Extension alloc] init] isNetworkExtensionEnabled]) { //dbg msg os_log_debug(logHandle, "network extension already enabled, jumping to 'configure' view"); //goto to next view! [self showView:self.configureView firstResponder:SHOW_SUPPORT]; //done break; } //show view [self showView:self.allowExtensionView firstResponder:-1]; //macOS 15 // set image and instructions if(@available(macOS 15.0, *)) { //set image self.approveExt.image = [NSImage imageNamed:@"InstallApprove"]; //set instructions self.approveExtMessage.stringValue = NSLocalizedString(@"2. In System Settings, toggle on the LuLu extension", @"2. In System Settings, toggle on the LuLu extension"); } //pre-macOS 15 // set (older) image and (older) instructions else { //set image self.approveExt.image = [NSImage imageNamed:@"InstallApprove_OLD"]; //set instructions self.approveExtMessage.stringValue = NSLocalizedString(@"2. In System Settings, scroll to where it mentions 'LuLu' and click 'Allow'", @"2. In System Settings, scroll to where it mentions 'LuLu' and click 'Allow'"); } //show message self.allowExtMessage.hidden = NO; //show spinner self.allowExtActivityIndicator.hidden = NO; //start spinner [self.allowExtActivityIndicator startAnimation:nil]; //in background // activate and wait for extension to be approved dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //extension Extension* extension = nil; //wait semaphore dispatch_semaphore_t semaphore = 0; //init extension object extension = [[Extension alloc] init]; //init wait semaphore semaphore = dispatch_semaphore_create(0); //kick off extension activation request [extension toggleExtension:ACTION_ACTIVATE reply:^(NSError* error) { //dbg msg os_log_debug(logHandle, "extension 'activate' returned"); //error if(error) { //err msg os_log_error(logHandle, "ERROR: failed to activate system extension"); //show alert/exit on main thread dispatch_async(dispatch_get_main_queue(), ^{ //show alert showAlert(NSAlertStyleCritical, NSLocalizedString(@"ERROR: activation failed", @"ERROR: activation failed"), NSLocalizedString(@"failed to activate system extension", @"failed to activate system extension"), @[NSLocalizedString(@"OK", @"OK")]); //bye [NSApplication.sharedApplication terminate:self]; }); } //dbg msg os_log_debug(logHandle, "system extension activated"); //update message on main thread dispatch_sync(dispatch_get_main_queue(), ^{ //update UI self.allowExtMessage.stringValue = NSLocalizedString(@"Waiting for Network Extension Approval", @"Waiting for Network Extension Approval"); }); //activate network extension if(YES != [extension toggleNetworkExtension:ACTION_ACTIVATE]) { //err msg os_log_error(logHandle, "ERROR: failed to activate network extension"); //show alert/exit on main thread dispatch_async(dispatch_get_main_queue(), ^{ //show alert showAlert(NSAlertStyleCritical, NSLocalizedString(@"ERROR: activation failed",@"ERROR: activation failed"), NSLocalizedString(@"failed to activate network extension",@"failed to activate network extension"), @[NSLocalizedString(@"OK", @"OK")]); //bye [NSApplication.sharedApplication terminate:self]; }); } //dbg msg os_log_debug(logHandle, "network filter approved/enabled"); //update UI dispatch_sync(dispatch_get_main_queue(), ^{ //hide spinner self.allowExtActivityIndicator.hidden = YES; //hide message self.allowExtMessage.hidden = YES; //goto to next view! [self showView:self.configureView firstResponder:SHOW_SUPPORT]; //make it key window [self.window makeKeyAndOrderFront:self]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //(re)make front [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; }); //signal semaphore dispatch_semaphore_signal(semaphore); }]; //wait for extension semaphore dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }); break; } //show configure view case SHOW_CONFIGURE: //show [self showView:self.configureView firstResponder:SHOW_SUPPORT]; break; //show "support us" view // + kick off main logic so traffic filtering is started case SHOW_SUPPORT: //show support view [self showView:self.supportView firstResponder:SUPPORT_YES]; //kick off main (client) logic [((AppDelegate*)[[NSApplication sharedApplication] delegate]) completeInitialization:self.preferences]; break; //support, yes! case SUPPORT_YES: //open patreon URL // invokes user's default browser [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:PATREON_URL]]; //fall thru as we want to close/set app state //support, no :( case SUPPORT_NO: //close window [self.window close]; //finally set app's background/foreground state [((AppDelegate*)[[NSApplication sharedApplication] delegate]) setActivationPolicy]; break; default: break; } return; } //show a view // note: replaces old view and highlights specified responder -(void)showView:(NSView*)view firstResponder:(NSInteger)firstResponder { //x position CGFloat xPos = 0; //y position CGFloat yPos = 0; //not in dark mode? // make window white if(YES != isDarkMode()) { //set white view.layer.backgroundColor = [NSColor whiteColor].CGColor; } //set content view size self.window.contentSize = view.frame.size; //update config view self.window.contentView = view; //center x xPos = NSWidth(self.window.screen.frame)/2 - NSWidth(self.window.frame)/2; //center y yPos = NSHeight(self.window.screen.frame)/2 - NSHeight(self.window.frame)/2; //center window [self.window setFrame:NSMakeRect(xPos, yPos, NSWidth(self.window.frame), NSHeight(self.window.frame)) display:YES]; //make 'next' button first responder // calling this without a timeout, sometimes fails :/ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ //set first responder if(-1 != firstResponder) { //first responder [self.window makeFirstResponder:[view viewWithTag:firstResponder]]; } }); return; } @end ================================================ FILE: LuLu/App/XPCDaemonClient.h ================================================ // // file: XPCDaemonClient.h // project: lulu (shared) // description: talk to daemon via XPC (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import Foundation; #import "XPCDaemonProto.h" @interface XPCDaemonClient : NSObject { } //xpc connection to daemon @property (atomic, strong, readwrite)NSXPCConnection* daemon; //get preferences // note: synchronous -(NSDictionary*)getPreferences; //update (save) preferences // note: synchronous, as then returns latest preferences -(NSDictionary*)updatePreferences:(NSDictionary*)preferences; //get rules // note: synchronous -(NSDictionary*)getRules; //add rule -(void)addRule:(NSDictionary*)info; //disable (or re-enable) rule -(void)toggleRule:(NSString*)key rule:(NSString*)uuid state:(NSNumber*)state; //delete rule -(void)deleteRule:(NSString*)key rule:(NSString*)uuid; //import rules -(BOOL)importRules:(NSData*)newRules userOnly:(BOOL)userOnly; //cleanup rules -(NSInteger)cleanupRules:(BOOL)full; //get current profile -(NSString*)getCurrentProfile; //get list of profiles -(NSMutableArray*)getProfiles; //set profile -(BOOL)setProfile:(NSString*)name; //add profile -(BOOL)addProfile:(NSString*)name preferences:(NSDictionary*)preferences; //delete profile -(BOOL)deleteProfile:(NSString*)name; //uninstall -(BOOL)uninstall; @end ================================================ FILE: LuLu/App/XPCDaemonClient.m ================================================ // // file: XPCDaemonClient.m // project: lulu (shared) // description: talk to daemon via XPC (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "XPCUser.h" #import "utilities.h" #import "AppDelegate.h" #import "XPCUserProto.h" #import "XPCDaemonClient.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //alert (windows) extern NSMutableDictionary* alerts; @implementation XPCDaemonClient @synthesize daemon; //init // create XPC connection & set remote obj interface -(id)init { //super self = [super init]; if(nil != self) { //alloc/init daemon = [[NSXPCConnection alloc] initWithMachServiceName:DAEMON_MACH_SERVICE options:0]; //set remote object interface self.daemon.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCDaemonProtocol)]; //set exported object interface (protocol) self.daemon.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCUserProtocol)]; //set exported object // this will allow daemon to invoke user methods! self.daemon.exportedObject = [[XPCUser alloc] init]; //resume [self.daemon resume]; } return self; } //get preferences // note: synchronous, will block until daemon responds -(NSDictionary*)getPreferences { //preferences __block NSDictionary* preferences = nil; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] getPreferences:^(NSDictionary* preferencesFromDaemon) { //dbg msg os_log_debug(logHandle, "got preferences: %{public}@", preferencesFromDaemon); //save preferences = preferencesFromDaemon; }]; return preferences; } //update (save) preferences // note: will merge into current ones -(NSDictionary*)updatePreferences:(NSDictionary*)preferences { //updated preferences (from daemon) __block NSDictionary* updatedPreferences = nil; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //update prefs [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] updatePreferences:preferences reply:^(NSDictionary* preferences) { //dbg msg os_log_debug(logHandle, "got preferences: %{public}@", preferences); //save updatedPreferences = preferences; }]; return updatedPreferences; } //get rules // note: synchronous, will block until daemon responds -(NSDictionary*)getRules { //rules __block NSMutableDictionary* rules = nil; //error __block NSError* error = nil; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //make XPC request to get rules [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] getRules:^(NSData* archivedRules) { //unarchive rules = [NSKeyedUnarchiver unarchivedObjectOfClasses: [NSSet setWithArray: @[[NSMutableDictionary class], [NSMutableArray class], [NSString class], [NSNumber class], [NSMutableSet class], [NSDate class], [Rule class]]] fromData:archivedRules error:&error]; if(nil != error) { //err msg os_log_error(logHandle, "ERROR: failed to unarchive rules: %{public}@", error); } }]; return rules; } //add rule -(void)addRule:(NSDictionary*)info { //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s' with info: %{public}@", __PRETTY_FUNCTION__, info); //make XPC request to add rule [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] addRule:info]; return; } //disable (or re-enable) rule -(void)toggleRule:(NSString*)key rule:(NSString*)uuid state:(NSNumber*)state { //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s' with key: %{public}@, rule id: %{public}@", __PRETTY_FUNCTION__, key, uuid); //disable rule [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] toggleRule:key rule:uuid state:state]; return; } //delete rule -(void)deleteRule:(NSString*)key rule:(NSString*)uuid { //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s' with key: %{public}@, rule id: %{public}@", __PRETTY_FUNCTION__, key, uuid); //delete rule [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] deleteRule:key rule:uuid]; return; } //cleanup rules -(NSInteger)cleanupRules:(BOOL)full { //result __block NSInteger deletedRules = -1; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //import rules [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] cleanupRules:full reply:^(NSInteger result) { //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s', done! (returned %ld)", __PRETTY_FUNCTION__, (long)deletedRules); //save result deletedRules = result; }]; return deletedRules; } //update (save) preferences -(BOOL)importRules:(NSData*)newRules userOnly:(BOOL)userOnly { //flag __block BOOL wasImported = NO; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //import rules [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] importRules:newRules userOnly:(BOOL)userOnly result:^(BOOL result) { //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s', done!", __PRETTY_FUNCTION__); //set flag wasImported = YES; }]; return wasImported; } //get current profile -(NSString*)getCurrentProfile { //rules __block NSString* currentProfile = nil; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //make XPC request to get profiles [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError* proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] getCurrentProfile:^(NSString* currrentProfileFromDaemon) { //dbg msg os_log_debug(logHandle, "current profile from daemon: '%{public}@'", currrentProfileFromDaemon); //save currentProfile = currrentProfileFromDaemon; }]; return currentProfile; } //get list of profiles -(NSMutableArray*)getProfiles { //rules __block NSMutableArray* profiles = nil; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //make XPC request to get profiles [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError* proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] getProfiles:^(NSArray* profilesFromDaemon) { //save profiles = [profilesFromDaemon mutableCopy]; }]; return profiles; } //set profile -(BOOL)setProfile:(NSString*)name { //flag __block BOOL wasSet = NO; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s' with name: %{public}@", __PRETTY_FUNCTION__, name); //send XPC message to set profile [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError* proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] setProfile:name reply:^(BOOL reply) { //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s', done!", __PRETTY_FUNCTION__); //set flag wasSet = reply; }]; return wasSet; } //add profile -(BOOL)addProfile:(NSString*)name preferences:(NSDictionary*)preferences { //flag __block BOOL wasAdded = NO; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s' with %{public}@", __PRETTY_FUNCTION__, name); //make XPC request to add profile [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError* proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] addProfile:name preferences:preferences reply:^(BOOL reply) { //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s', done!", __PRETTY_FUNCTION__); //set flag wasAdded = reply; }]; return wasAdded; } //delete profile -(BOOL)deleteProfile:(NSString*)name { //flag __block BOOL wasDeleted = NO; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s' with name: %{public}@", __PRETTY_FUNCTION__, name); //send XPC message to delete profile [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError* proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] deleteProfile:name reply:^(BOOL reply) { //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s', done!", __PRETTY_FUNCTION__); //set flag wasDeleted = reply; }]; //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s' with name: %{public}@ returned", __PRETTY_FUNCTION__, name); return wasDeleted; } //uninstall -(BOOL)uninstall { //flag __block BOOL uninstalled = NO; //dbg msg os_log_debug(logHandle, "invoking daemon XPC method, '%s'", __PRETTY_FUNCTION__); //uninstall [[self.daemon synchronousRemoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); }] uninstall:^(BOOL result) { //dbg msg os_log_debug(logHandle, "daemon XPC method, '%s', done!", __PRETTY_FUNCTION__); //set flag uninstalled = result; }]; return uninstalled; } @end ================================================ FILE: LuLu/App/XPCUser.h ================================================ // // file: XPCUser.h // project: lulu (login item) // description: user XPC methods (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // @import OSLog; @import Foundation; #import "XPCUserProto.h" @interface XPCUser : NSObject { } @end ================================================ FILE: LuLu/App/XPCUser.m ================================================ // // file: XPCUser.m // project: lulu (login item) // description: user XPC methods // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // #import "consts.h" #import "XPCUser.h" #import "utilities.h" #import "AppDelegate.h" #import "AlertWindowController.h" @implementation XPCUser /* GLOBALS */ //log handle extern os_log_t logHandle; //alert (windows) extern NSMutableDictionary* alerts; //show an alert window -(void)alertShow:(NSDictionary*)alert reply:(void (^)(NSDictionary*))reply { //dbg msg os_log_debug(logHandle, "daemon invoked user XPC method, '%s', with %{public}@", __PRETTY_FUNCTION__, alert); //on main (ui) thread dispatch_sync(dispatch_get_main_queue(), ^{ //alert window AlertWindowController* alertWindow = nil; //alloc/init alert window alertWindow = [[AlertWindowController alloc] initWithWindowNibName:@"AlertWindow"]; //sync to save alert // ensures there is a (memory) reference to the window @synchronized(alerts) { //save alerts[alert[KEY_UUID]] = alertWindow; } //set reply alertWindow.reply = reply; //set alert alertWindow.alert = alert; //show in all spaces alertWindow.window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces; //show alert window [alertWindow showWindow:self]; //make alert window key [alertWindow.window makeKeyAndOrderFront:self]; //set app's background/foreground state [((AppDelegate*)[[NSApplication sharedApplication] delegate]) setActivationPolicy]; //request user attention // bounces icon on the dock [NSApp requestUserAttention:NSCriticalRequest]; //delay, then make the alert window front // note: this will stop the dock bouncing... dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //make it modal(ish) [alertWindow.window setLevel:NSPopUpMenuWindowLevel]; //code sign change? // show code signing popover if(YES == [alert[KEY_CS_CHANGE] boolValue]) { //dbg msg os_log_debug(logHandle, "code signing information changed, will show (modal) alert to user"); //invoke handler to open [alertWindow openSigningInfoPopover]; //show (modal) alert dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //lower window level (so alert can show above) [alertWindow.window setLevel:NSNormalWindowLevel]; //alert showAlert(NSAlertStyleInformational, [NSString stringWithFormat:NSLocalizedString(@"%@'s code signing information has changed", @"%@'s code signing information has changed"), alert[KEY_PROCESS_NAME]], @"", @[NSLocalizedString(@"OK", @"OK")]); //(re)set window level [alertWindow.window setLevel:NSPopUpMenuWindowLevel]; }); } }); }); //reverse dns resolve ip // background resolve, then update alert window if(nil != alert[KEY_HOST]) { //async // resolve ip -> host dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //responses NSArray* responses = nil; //address NSString* address = nil; //capture address = alert[KEY_HOST]; //resolve responses = resolveAddress(address); //dbg msg os_log_debug(logHandle, "resolved %{public}@ to %{public}@", address, responses); //sync to add to alert window(s) @synchronized(alerts) { //find any who's ip matches [alerts enumerateKeysAndObjectsUsingBlock:^(id key, AlertWindowController* alertWindow, BOOL* stop) { //match? // update alert window if(YES == [alertWindow.alert[KEY_HOST] isEqualToString:address]) { //update window on main thread dispatch_async(dispatch_get_main_queue(), ^{ //response NSString* response = nil; //set response = responses.firstObject; //error/not found? if(0 == response.length) { //set default response = NSLocalizedString(@"unknown", @"unknown"); } //set text alertWindow.reverseDNS.string = response; //wrapping [alertWindow setWrapping:alertWindow.reverseDNS]; //set tooltip alertWindow.reverseDNS.toolTip = [NSString stringWithFormat:NSLocalizedString(@"Reverse Domain: %@", @"Reverse Domain %@"), alertWindow.reverseDNS.string]; }); } }]; } }); } return; } //rule changed // broadcast new rules, so any (relevant) windows can be updated -(void)rulesChanged { //dbg msg os_log_debug(logHandle, "daemon invoked user XPC method, '%s'", __PRETTY_FUNCTION__); //broadcast [[NSNotificationCenter defaultCenter] postNotificationName:RULES_CHANGED object:nil userInfo:nil]; return; } @end ================================================ FILE: LuLu/App/main.m ================================================ // // main.m // LuLu // // Created by Patrick Wardle on 8/1/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "Configure.h" @import Cocoa; @import OSLog; //TODO: // new pref: allow apple programs, unless has args + parent is untrusted // add rule option: args /* GLOBALS */ //log handle os_log_t logHandle = nil; int main(int argc, const char * argv[]) { //status int status = -1; //config obj Configure* configure = nil; //pool @autoreleasepool { //init log logHandle = os_log_create(BUNDLE_ID, "application"); //dbg msg(s) os_log_debug(logHandle, "started: %{public}@ (pid: %d / uid: %d)", NSProcessInfo.processInfo.arguments.firstObject, getpid(), getuid()); os_log_debug(logHandle, "arguments: %{public}@", NSProcessInfo.processInfo.arguments); /* cmdline interface - for install/upgrade/uninstall */ //install? if(YES == [NSProcessInfo.processInfo.arguments containsObject:@"-install"]) { //first check root if(0 != geteuid()) { //err msg printf("\nLULU ERROR: cmdline interface actions require root\n\n"); goto bail; } //init configure = [[Configure alloc] init]; //dbg msg os_log_debug(logHandle, "performing cmdline install"); //install if(YES != [configure install]) { //error printf("\nLULU ERROR: install failed (see system log for details)\n\n"); goto bail; } //dbg msg printf("\nLULU: installed\n\n"); //done goto bail; } //upgrade? else if(YES == [NSProcessInfo.processInfo.arguments containsObject:@"-upgrade"]) { //first check root if(0 != geteuid()) { //err msg printf("\nLULU ERROR: cmdline interface actions require root\n\n"); goto bail; } //init configure = [[Configure alloc] init]; //dbg msg os_log_debug(logHandle, "performing cmdline upgrade"); //upgrade if(YES != [configure upgrade]) { //error printf("\nLULU ERROR: upgrade failed (see system log for details)\n\n"); goto bail; } //dbg msg printf("\nLULU: upgraded\n\n"); //done goto bail; } //uninstall? if(YES == [NSProcessInfo.processInfo.arguments containsObject:@"-uninstall"]) { //first check root if(0 != geteuid()) { //err msg printf("\nLULU ERROR: cmdline interface actions require root\n\n"); goto bail; } //init configure = [[Configure alloc] init]; //dbg msg os_log_debug(logHandle, "performing cmdline uninstall"); //uninstall if(YES != [configure uninstall]) { //error printf("\nLULU ERROR: uninstall failed (see system log for details)\n\n"); goto bail; } //dbg msg printf("\nLULU: uninstalled\n\n"); //done goto bail; } //quit? // this is the copy, to (just) deactivate extension if(YES == [NSProcessInfo.processInfo.arguments containsObject:@"-quit"]) { //init configure = [[Configure alloc] init]; //dbg msg os_log_debug(logHandle, "performing cmdline quit"); //quit [configure quit]; //done goto bail; } //invalid args // just print msg, for cmdline case else if(NSProcessInfo.processInfo.arguments.count > 1) { //err msg printf("\nLULU ERROR: %s are not valid args\n\n", NSProcessInfo.processInfo.arguments.description.UTF8String); } //main app interface status = NSApplicationMain(argc, argv); } //pool bail: return status; } ================================================ FILE: LuLu/App/mul.lproj/AboutWindow.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "6g3-Pg-x3P.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Support Us!\"; ObjectID = \"6g3-Pg-x3P\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unterstütze uns!" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Support Us!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Apóyanos!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Soutenez nous !" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sostienici!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "저희를 응원해 주세요!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wesprzyj nas!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Suporte-nos!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bizi Destekleyin!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримайте нас!" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ہماری حمایت کریں!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "支持我们!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "支持我們!" } } } }, "bBK-v0-ypq.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Version:\"; ObjectID = \"bBK-v0-ypq\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Version:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Version : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Версія:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ورژن:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "版本:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版本:" } } } }, "fJg-qw-wDf.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Patrons & Friends\"; ObjectID = \"fJg-qw-wDf\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unterstützer & Freunde:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Patrons & Friends" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Patrons & Friends" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mécènes et amis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sostenitori e Amici" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Patron 후원자 & 친구들" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Patroni i Przyjaciele" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinadores e Amigos" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aboneler ve Arkadaşlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Патрони і друзі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "سرپرست اور دوست" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "赞助者&伙伴" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "贊助者 & 朋友" } } } }, "J9x-sM-h9S.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"More Information\"; ObjectID = \"J9x-sM-h9S\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weitere Informationen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "More Information" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Más Información" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Plus d’information" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Maggiori informazioni" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "더 많은 정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Więcej informacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mais Informações" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha Fazla Bilgi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Більше інформації" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مزید معلومات" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更多资讯" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更多資訊" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/AddRule.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "5ic-Om-d8g.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Remote port\"; ObjectID = \"5ic-Om-d8g\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Remote Port" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Remote port" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Puerto remoto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Port distant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Porta remota" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "원격 포트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zdalny port" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Porta remota" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzak kapı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Віддалений порт" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ریموٹ پورٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "远程端口" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "遠端連接埠" } } } }, "7yf-wW-FZC.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Remote address/domain\"; ObjectID = \"7yf-wW-FZC\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Remote Adresse/Domain" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Remote address/domain" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dirección remota/dominio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Adresse/domaine distant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Indirizzo/Dominio remoto" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "원격 주소/도메인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zdalny adres/domena" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Endereço/domínio remoto" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzak adres/Etki alanı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Віддалена адреса/домен" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ریموٹ ایڈریس/ڈومین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "远程地址/域" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "遠端位址/網域" } } } }, "F0z-JX-Cv5.title" : { "comment" : "Class = \"NSWindow\"; title = \"Add Rule (for outgoing connections)\"; ObjectID = \"F0z-JX-Cv5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regel hinzufügen (für ausgehende Verbidungen)" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Add Rule (for outgoing connections)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar Regla (para conexiones salientes)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter une règle (pour connections sortantes)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi regola (per connessioni in uscita)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 추가 (나가는 연결)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj regułę (dla połączeń wychodzących)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar regra (para conexões de saída)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural Ekle (Dışarı Giden Bağlantılar)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати правило (для вихідних з'єднань)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قانون شامل کریں (باہر جانے والے کنکشنز کے لیے)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增规则(适用对外链接)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增規則(適用於對外連線)" } } } }, "FCA-Cv-yT3.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"regex?\"; ObjectID = \"FCA-Cv-yT3\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "regex?" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "regex?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "regex?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "regex ?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정규 표현식?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "regex?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "expressão regular?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Düzenli ifade" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "regex?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ریگولر ایکسپریشن؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正则表达式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正規表達式?" } } }, "shouldTranslate" : false }, "fX0-Jq-Okq.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Cancel\"; ObjectID = \"fX0-Jq-Okq\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abbrechen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Cancel" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annuler" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Annulla" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "취소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Anuluj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Vazgeç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "منسوخ کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消" } } } }, "JJl-Lz-apr.title" : { "comment" : "Class = \"NSButtonCell\"; title = \" Block\"; ObjectID = \"JJl-Lz-apr\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " Blockieren" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " Block" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bloquer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : " Blocca" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : " 차단" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Blokuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Блокувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋" } } } }, "kXW-Xv-Fc4.placeholderString" : { "comment" : "Class = \"NSTextFieldCell\"; placeholderString = \"*\"; ObjectID = \"kXW-Xv-Fc4\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "*" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "*" } } }, "shouldTranslate" : false }, "oCK-aR-Ek4.title" : { "comment" : "Class = \"NSButtonCell\"; title = \" Allow\"; ObjectID = \"oCK-aR-Ek4\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " Erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " Allow" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : " Consenti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : " 허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pozwól" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin Ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許" } } } }, "OoX-aE-Hgf.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Browse\"; ObjectID = \"OoX-aE-Hgf\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Durchsuchen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Browse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Navegar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Parcourir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sfoglia" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "찾아보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przeglądaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Buscar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Göz At" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Огляд" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تلاش کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "浏览" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "瀏覽" } } } }, "QWe-2U-lfn.placeholderString" : { "comment" : "Class = \"NSTextFieldCell\"; placeholderString = \"*\"; ObjectID = \"QWe-2U-lfn\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "*" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "*" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "*" } } }, "shouldTranslate" : false }, "tc1-pS-Wns.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Program path\"; ObjectID = \"tc1-pS-Wns\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programmpfad" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Program path" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta del programa" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin du programme" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso programma" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로그램 경로" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka programu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho do programa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Program yolu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях до програми" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروگرام پاتھ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "程序路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "程式路徑" } } } }, "V5P-DF-wCP.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Add\"; ObjectID = \"V5P-DF-wCP\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzufügen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Add" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اضافہ کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/AlertWindow.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "1zg-95-QM7.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \" -\"; ObjectID = \"1zg-95-QM7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : " - Zertifizierungsinstanz 2" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " -" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "- autorización de firma 2" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : " - autorité de signature 2" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : " - 인증 기관 2" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "- podpisanie autoryzacji 2" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "- imzalama otoritesi 2" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " - سرٹیفیکیشن اتھارٹی 2" } } }, "shouldTranslate" : false }, "8eV-Ag-i7n.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Connection:\"; ObjectID = \"8eV-Ag-i7n\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verbindungen:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Connection:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Conexión:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Connection : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Connessione:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Połączenie:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Conexão:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bağlantı:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "З'єднання:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کنکشن:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "链接内容:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "連線:" } } } }, "9OD-rL-0J2.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Rule Duration:\"; ObjectID = \"9OD-rL-0J2\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeldauer:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rule Duration:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Duración de la Regla:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Durée de la règle : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Durata regola:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 지속 시간:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czas trwania reguły:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Duração da Regra:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural süresi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тривалість правила:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قانون کی مدت:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则持续时间:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則持續時間:" } } } }, "31V-e4-dgv.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"port/protocol:\"; ObjectID = \"31V-e4-dgv\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Port/Protokoll:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "port/protocol:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "puerto/proto:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "port/protocole : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "porta/protocollo:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "포트/프로토콜:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "port/protokół:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "porta/protocolo:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "kapı/protokol" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "порт/протокол:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پورٹ/پروٹوکول:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "端口/通讯协议" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "連接埠/通訊協定:" } } } }, "80z-6q-eWC.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Process lifetime\"; ObjectID = \"80z-6q-eWC\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozesslaufzeit" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Process lifetime" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Duración del proceso" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Durée de vie du processus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Durata processo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스 수명 동안" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czas życia procesu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Duração do processo:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem yaşam süresi boyunca" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тривалість життя процесу" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "عمل کی مدت" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "持续到进程结束" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "持續到此 process 結束" } } } }, "abH-g6-95w.placeholderString" : { "comment" : "Class = \"NSTextFieldCell\"; placeholderString = \"HH\"; ObjectID = \"abH-g6-95w\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "HH" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "SS:dd" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "ГГ:хх" } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "HH:mm" } } }, "shouldTranslate" : false }, "AuV-oM-p6G.placeholderString" : { "comment" : "Class = \"NSTextFieldCell\"; placeholderString = \"mm\"; ObjectID = \"AuV-oM-p6G\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "mm" } } }, "shouldTranslate" : false }, "b63-At-csb.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Details:\"; ObjectID = \"b63-At-csb\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Details:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Details:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Detalles:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détails : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dettagli:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "세부 사항:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Szczegóły:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Detalhes:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıntılar:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Деталі:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تفصیلات:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "详情:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "詳細資訊" } } } }, "cUm-e3-Dh5.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \":\"; ObjectID = \"cUm-e3-Dh5\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : ":" } } }, "shouldTranslate" : false }, "D1C-aK-AFU.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Signing Authorities:\"; ObjectID = \"D1C-aK-AFU\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zertifizierungsinstanzen:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Signing Authorities:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Autorizaciones de Firma:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorités de signature : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Autorità di firma:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "인증 기관:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uwierzytelnianie podpisów:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Assinaturas de Autorizações:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İmzalama otoriteleri:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Довірені підписанти:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "سرٹیفیکیشن اتھارٹیز:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "签名机构:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "簽署單位" } } } }, "F0z-JX-Cv5.title" : { "comment" : "Class = \"NSWindow\"; title = \"LuLu Alert\"; ObjectID = \"F0z-JX-Cv5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu Mitteilung" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu Alert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Alerta de LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Alert LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avviso LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 경고" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Alert LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alerta LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu İkâzı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сповіщення LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu الارٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu警告" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 警告" } } } }, "FLb-1c-otA.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \" -\"; ObjectID = \"FLb-1c-otA\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : " - Zertifizierungsinstanz 3" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " -" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "- autorización de firma 3" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : " - autorité de signature 3" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : " - 인증 기관 3" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "- podpisanie autoryzacji 3" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "- imzalama otoritesi 3" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " - سرٹیفیکیشن اتھارٹی 3" } } }, "shouldTranslate" : false }, "Fxr-aW-bT2.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"(reverse) dns:\"; ObjectID = \"Fxr-aW-bT2\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "(Reverse) DNS: " } }, "en" : { "stringUnit" : { "state" : "new", "value" : "(reverse) dns:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "dns (inverso):" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "dns (inverse) : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "DNS inverso:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "(역방향) DNS:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "(reverse) dns:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "DNS (reverso):" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "(ters) dns:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "(зворотний) DNS:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "(ریورس) ڈی این ایس:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(反向)DNS:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "(反向)DNS:" } } } }, "FYF-6N-pro.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Details & Options\"; ObjectID = \"FYF-6N-pro\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Details & Optionen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Details & Options" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Detalles & Opciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détails et options" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dettagli e opzioni" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "세부 사항 & 옵션" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Szczegóły i opcje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Detalhes e Opções" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıntılar ve Seçenekler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Деталі та параметри" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تفصیلات اور آپشنز" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "详情&选项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "詳細資訊 & 選項" } } } }, "G2Q-QQ-EX7.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Rule Scope:\"; ObjectID = \"G2Q-QQ-EX7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regel-Geltungsbereich:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rule Scope:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Alcance de la regla:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Champ d’application de la règle : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dettagli e opzioni" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 적용 범위:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zakres reguły:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alcance da Regra:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural kapsamı:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Межі правила:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قانون کا دائرہ کار:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则作用范围:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則範圍" } } } }, "Gqr-cR-TAF.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"path:\"; ObjectID = \"Gqr-cR-TAF\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "path:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ruta:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "chemin : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "percorso:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "ścieżka:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "caminho:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "yol:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "шлях:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پاتھ:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "路径:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "路徑:" } } } }, "HZu-Op-XHi.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"pid:\"; ObjectID = \"HZu-Op-XHi\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "pid:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "pid : " } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "PID:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پی آئی ڈی:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "pid:" } } }, "shouldTranslate" : false }, "Iys-aY-OZ4.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Process\"; ObjectID = \"Iys-aY-OZ4\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozess" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Process" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Proceso" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Processus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Processo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Proces" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Processo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Процес" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "عمل" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进程" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Process" } } } }, "JEi-1U-vkU.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Process Name\"; ObjectID = \"JEi-1U-vkU\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozessname" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Process Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre del Proceso" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom du processus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome processo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa procesu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome do Processo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem Adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва процесу" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "عمل کا نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进程名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Process 名稱" } } } }, "JFW-5C-nvy.headerCell.title" : { "comment" : "Class = \"NSTableColumn\"; headerCell.title = \"Name\"; ObjectID = \"JFW-5C-nvy\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Name" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ad" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "名稱" } } } }, "Ki9-ld-5Xu.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \" -\"; ObjectID = \"Ki9-ld-5Xu\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : " - Zertifizierungsinstanz 1" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " -" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "- autorización de firma 1" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : " - autorité de signature 1" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : " - 인증 기관 1" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "- podpisanie autoryzacji 1" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "- imzalama otoritesi 1" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " - سرٹیفیکیشن اتھارٹی 1" } } }, "shouldTranslate" : false }, "N6E-eT-Q39.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Remote Endpoint\"; ObjectID = \"N6E-eT-Q39\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Remote-Endpunkt" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Remote Endpoint" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Endpoint Remoto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Point d’accès distant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Endpoint remoto" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "원격 엔드포인트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zdalny punkt końcowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Endpoint remote" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzak Bitiş Noktası" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Віддалена веб- або IP-адреса" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ریموٹ اینڈ پوائنٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "远程端点" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "遠端端點" } } } }, "Pp2-2W-YkT.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Process:\"; ObjectID = \"Pp2-2W-YkT\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozess:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Process:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Proceso:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Processus : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Processo:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Proces:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Processo:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Процес:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "عمل:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进程:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Process:" } } } }, "Ssw-3u-wEC.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"message\"; ObjectID = \"Ssw-3u-wEC\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nachricht" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "message" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "mensaje" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "message" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "messaggio" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "메시지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "wiadomość" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "mensagem" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mesaj" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "повідомлення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پیغام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "訊息" } } } }, "T20-eE-vRR.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"args:\"; ObjectID = \"T20-eE-vRR\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Argumente:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "args:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "args:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "arguments : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "argomenti:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "인수:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "argumenty:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "argumentos:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "argümanlar:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "аргументи:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ارجومنٹس:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "参数:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "參數:" } } } }, "Ubf-5c-Fd2.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Always\"; ObjectID = \"Ubf-5c-Fd2\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Immer" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Always" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siempre" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Toujours" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sempre" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항상" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zawsze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sempre" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Her zaman" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завжди" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ہمیشہ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "总是" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "總是" } } } }, "vmZ-KS-uuK.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"ip address:\"; ObjectID = \"vmZ-KS-uuK\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "IP-Adresse:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "ip address:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "dirección ip:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "adresse ip : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "indirizzo IP:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "IP 주소:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "adres IP:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "endereço de ip:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "ip adresi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "IP-адреса" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "آئی پی پتہ:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "IP地址:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "IP 位址:" } } } }, "W18-Ba-X7J.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Block\"; ObjectID = \"W18-Ba-X7J\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockieren" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Block" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bloquer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Blocca" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Blokuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Блокувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋" } } } }, "Wn5-T5-boq.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Expires in:\"; ObjectID = \"Wn5-T5-boq\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Läuft ab in:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Expires in:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Expira en:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Expire dans :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scade tra:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "만료까지:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wygasa za:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Expira em:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Süre dolmasına:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закінчується через:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "میعاد ختم ہونے میں:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "过期时间:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "過期時間:" } } } }, "ybM-PR-IbA.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Time stamp: \"; ObjectID = \"ybM-PR-IbA\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitstempel:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Time stamp: " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Marca de tiempo:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Horodatage : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Marca temporale:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "타임 스탬프:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sygnatura czasowa:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Registro da hora:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Zaman damgası:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Мітка часу:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ٹائم اسٹیمپ:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "時間戳:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "時間戳:" } } } }, "Zk5-p6-VV3.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Allow\"; ObjectID = \"Zk5-p6-VV3\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allow" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pozwól" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin Ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/ItemPaths.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "NhE-Zh-Scd.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Path(s):\"; ObjectID = \"NhE-Zh-Scd\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad(e):" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Path(s):" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta(s):" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin(s) : " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso/i:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka(i):" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho(s):" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yollar:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях(и):" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "راستہ/راستے:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "路径:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "路徑:" } } } }, "ox8-oX-X3s.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Close\"; ObjectID = \"ox8-oX-X3s\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Schließen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Close" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Chiudi" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "닫기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zamknij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fechar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kapat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بند کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/MainMenu.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "0Y1-Pq-6Fc.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Show...\"; ObjectID = \"0Y1-Pq-6Fc\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Anzeigen…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Show..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "보이기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Göster…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "دکھائیں..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示…" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示..." } } } }, "1HU-j3-Oa3.title" : { "comment" : "Class = \"NSMenu\"; title = \"Switch\"; ObjectID = \"1HU-j3-Oa3\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wechseln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Switch" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cambiar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Basculer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cambia" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전환" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przełącz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alternar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değiştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемкнути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تبدیل کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "切換" } } } }, "4Uv-rm-VzQ.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Settings...\"; ObjectID = \"4Uv-rm-VzQ\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einstellungen…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Settings..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Configuración..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réglages…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impostazioni…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설정..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ustawienia..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Configurações…" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayarlar…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Параметри…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ترتیبات..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设置…" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定…" } } } }, "7kG-TA-f15.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Profiles\"; ObjectID = \"7kG-TA-f15\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profiles" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfiles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profils" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profili" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfis" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profiller" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профілі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائلز" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔" } } } }, "39l-3u-dUR.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Import...\"; ObjectID = \"39l-3u-dUR\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Importieren…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Import..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Importar..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Importer…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Importa…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "가져오기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Import..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Importar…" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İçe Aktar…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Імпортувати…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "درآمد کریں..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导入…" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "匯入..." } } } }, "ahR-T8-occ.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Switch\"; ObjectID = \"ahR-T8-occ\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wechseln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Switch" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cambiar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Basculer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cambia" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전환" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przełącz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alternar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değiştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемкнути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تبدیل کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "切換" } } } }, "aJV-EX-6bj.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"LuLu\"; ObjectID = \"aJV-EX-6bj\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } } }, "shouldTranslate" : false }, "aZa-10-Ir1.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Export...\"; ObjectID = \"aZa-10-Ir1\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Exportieren…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Export..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Exportar..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exporter…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esporta…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "내보내기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Eksport..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exportar…" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dışa Aktar…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Експортувати…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "برآمد کریں..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出…" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "匯出..." } } } }, "d49-Ze-8oD.title" : { "comment" : "Class = \"NSMenu\"; title = \"Profiles\"; ObjectID = \"d49-Ze-8oD\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profiles" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfiles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profils" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profili" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfis" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profiller" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профілі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائلز" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔" } } } }, "DEa-wY-se7.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Quit LuLu\"; ObjectID = \"DEa-wY-se7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu beenden" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Quit LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Chiudi LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 종료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyjdź z LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fechar LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu'dan Çık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کو ختم کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉 LuLu" } } } }, "gXq-yc-QnA.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Add...\"; ObjectID = \"gXq-yc-QnA\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzufügen…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Add..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ekle…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اضافی کریں..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增..." } } } }, "J1I-fS-R65.title" : { "comment" : "Class = \"NSMenu\"; title = \"LuLu\"; ObjectID = \"J1I-fS-R65\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } } }, "shouldTranslate" : false }, "K0B-xd-gGB.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Support LuLu 💕\"; ObjectID = \"K0B-xd-gGB\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu unterstützen 💕" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Support LuLu 💕" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Apoyar LuLu 💕" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Soutenir LuLu 💕" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Supporta LuLu 💕" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 지원하기 💕" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wspieraj LuLu 💕" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Apoiar LuLu 💕" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu'yu Destekle 💕" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримати LuLu 💕" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کی حمایت کریں 💕" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "支持 LuLu 💕" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "支持 LuLu 💕" } } } }, "l9M-p8-kao.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Disable\"; ObjectID = \"l9M-p8-kao\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deaktivieren" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Disable" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Deshabilitar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disattiva" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desabilitar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Devre Dışı Bırak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ناکار کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用" } } } }, "lns-C6-Aa5.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Uninstall LuLu\"; ObjectID = \"lns-C6-Aa5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu deinstallieren" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Uninstall LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu'yu Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو ان انسٹال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝 LuLu" } } } }, "Lse-DT-C0z.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Cleanup\"; ObjectID = \"Lse-DT-C0z\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aufräumen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Cleanup" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpiar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyage" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pulisci" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Posprzątaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "صاف کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清理" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清理" } } } }, "mxA-q2-fvN.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Profile: Default\"; ObjectID = \"mxA-q2-fvN\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil: Standard" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profile: Default" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil: Predeterminado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil : Par défaut" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo: Predefinito" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필: 기본값" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profil: Domyślny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil: Padrão" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil: Saptanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профіль: За замовчуванням" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل: ڈیفالٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件:Default" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔:預設" } } } }, "pF8-Hh-0tW.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Manage...\"; ObjectID = \"pF8-Hh-0tW\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwalten…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Manage..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Gestionar..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestisci…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "관리..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzaj…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciar..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yönet…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Керувати…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "منظم کریں..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理..." } } } }, "q1D-fP-yr1.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"About LuLu\"; ObjectID = \"q1D-fP-yr1\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Über LuLu" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "About LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acerca de LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "À propos de LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Informazioni su LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu에 관하여" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "O LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sobre LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu Hakkında" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Про LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کے بارے میں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关于LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關於 LuLu" } } } }, "QrZ-U2-1Ib.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Quit LuLu\"; ObjectID = \"QrZ-U2-1Ib\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu beenden" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Quit LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Chiudi LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 종료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyjdź z LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fechar LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu'dan Çık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کو ختم کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉 LuLu" } } } }, "uhX-d8-iZM.title" : { "comment" : "Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"uhX-d8-iZM\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hauptmenü" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Main Menu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Menú Principal" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Menu Principal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Menu Principale" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "메인 매뉴" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Menu główne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Menu Principal" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ana Menü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Головне меню" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مینیو" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主菜单" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "主選單" } } } }, "UuL-hi-gEf.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Rules\"; ObjectID = \"UuL-hi-gEf\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則" } } } }, "vb5-99-ejR.title" : { "comment" : "Class = \"NSMenu\"; title = \"Rules\"; ObjectID = \"vb5-99-ejR\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則" } } } }, "VuY-Sy-392.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Network Monitor...\"; ObjectID = \"VuY-Sy-392\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Netzwerkmonitor…" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Network Monitor..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Monitor de Red..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Moniteur réseau…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Monitor di Rete…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "네트워크 모니터..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Monitor sieci..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Monitor de Rede" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ağ Monitörü…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Монітор мережі.." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نیٹ ورک مانیٹر..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "网络监控…" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "網路監控器..." } } } }, "w99-Wo-epe.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"LULU: enabled\"; ObjectID = \"w99-Wo-epe\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LULU: aktiviert" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LULU: enabled" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LULU: habilitado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LULU : activé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LULU: abilitato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LULU: 활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LULU: włączona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LULU: habilitado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: Etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: увімкнено" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو: فعال" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: 可用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LULU:啟用" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/Preferences.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "2qr-2x-A3U.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Check Now\"; ObjectID = \"2qr-2x-A3U\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Jetzt prüfen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Check Now" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Verificar Ahora" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Verifica ora" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지금 확인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sprawdź teraz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificar Agora" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şimdi Denetle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перевірити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ابھی چیک کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "立即检查" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "現在檢查" } } } }, "4EN-j2-enV.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Block Mode\"; ObjectID = \"4EN-j2-enV\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockierungsmodus" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Block Mode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo Bloqueo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode blocage" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità Blocco" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단 모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tryb blokady" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear Modo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelleme kipi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Режим блокування" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک موڈ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋模式" } } } }, "5Fx-eu-v3J.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Cancel\"; ObjectID = \"5Fx-eu-v3J\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abbrechen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Cancel" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annuler" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Annulla" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "취소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Anuluj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İptal" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "منسوخ کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消" } } } }, "5li-Ut-Gsb.label" : { "comment" : "Class = \"NSToolbarItem\"; label = \"Profiles\"; ObjectID = \"5li-Ut-Gsb\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profiles" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfiles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profils" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profili" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfis" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profiller" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профілі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائلز" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔" } } } }, "5li-Ut-Gsb.paletteLabel" : { "comment" : "Class = \"NSToolbarItem\"; paletteLabel = \"Profiles\"; ObjectID = \"5li-Ut-Gsb\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profiles" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfiles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profils" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profili" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profile" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfis" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profiller" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профілі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائلز" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔" } } } }, "5Wo-JL-Khb.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Rules for new connections should be created?\"; ObjectID = \"5Wo-JL-Khb\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sollen Regeln für neue Verbindungen erstellt werden?" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rules for new connections should be created?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Se deben crear reglas para nuevas conexiones?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Faut-il créer des règles pour les nouvelles connexions ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Creare regole per le nuove connessioni?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 연결에 관한 규칙을 생성할까요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czy należy stworzyć reguły dla nowych połączeń?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Criar regras para novas conexões?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni bağlantılar için kurallar oluşturulmalı mı?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Створювати правила для нових з'єднань?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نئے کنکشنز کے لیے قواعد بنائے جائیں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "是否应创建新连接规则?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "是否應建立新連線的規則?" } } } }, "6gH-HW-CFe.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Outgoing connections from currently installed 3rd-party programs will be allowed.\"; ObjectID = \"6gH-HW-CFe\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Bereits installierte Drittanbieterprogramme werden erlaubt." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Outgoing connections from currently installed 3rd-party programs will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Se permitirán los programas de terceros previamente instalados." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Programmes précédemment installées, les programmes de tierce-partie seront autorisés." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "I programmi di terze parti già installati saranno consentiti." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이전에 설치한 타사 프로그램을 허용합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wcześniej zainstalowane programy innych firm będą dozwolone." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Programas terceirizados previamente instalados serão permitidos." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Daha önceden kurulmuş, 3. parti uygulamalara izin verilecektir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Раніше встановлені сторонні програми будуть дозволені." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "پہلے سے انسٹال شدہ، تیسرے فریق کے پروگراموں کو اجازت دی جائے گی۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "先前安装的第三方程序将被允许。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "已安裝的第三方程式將被允許" } } } }, "6vU-GO-opk.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Note: When a new connection is made, sometimes only the IP address may be available. In such cases, domains on the allow or block list cannot be matched, which may impact whether a request is blocked or allowed.\"; ObjectID = \"6vU-GO-opk\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinweis: Wenn eine neue Verbindung hergestellt wird, ist manchmal nur die IP-Adresse verfügbar. In solchen Fällen können die Domains in der Erlauben- oder Blockieren-Liste nicht abgeglichen werden, was sich darauf auswirken kann, ob eine Anfrage blockiert oder erlaubt wird." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Note: When a new connection is made, sometimes only the IP address may be available. In such cases, domains on the allow or block list cannot be matched, which may impact whether a request is blocked or allowed." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nota: cuando se hace una nueva conexión, a veces solo la dirección IP estará disponible. En esos casos, los dominios en la lista de permitidos o bloqueados no coincidirán, lo cual impactará si un pedido es bloqueado o permitido. " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Note : lorsqu'une nouvelle connexion est établie, il arrive que seule l'adresse IP soit disponible. Dans ce cas, les domaines figurant sur la liste d'autorisation ou de blocage ne peuvent être mis en correspondance, ce qui peut avoir une incidence sur le fait que la demande soit bloquée ou autorisée." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nota: Quando viene stabilita una nuova connessione, a volte è disponibile solo l’indirizzo IP. In questi casi, i domini nell’elenco di blocco o autorizzazione potrebbero non essere riconosciuti, il che può influire sul fatto che una richiesta venga bloccata o consentita." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "참고: 새로운 연결이 만들어질 때, 때때로 IP 주소만 이용 가능할 수 있습니다. 이러한 경우, 허용 리스트 또는 차단 리스트의 도메인과 일치시킬 수 없으며, 이로 인해 요청이 차단되거나 허용될지에 영향이 있을 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uwaga: Kiedy nawiązywane jest nowe połączenie, czasami dostępny może być tylko adres IP. W takich przypadkach domeny na liście dozwolonych lub blokowanych nie mogą zostać dopasowane, co może mieć wpływ na to, czy żądanie zostanie zablokowane, czy dozwolone." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nota: quando uma nova conexão é feita, às vezes somente o endereço de IP estará disponível. Nestes casos, os domínios nas listas de permissão ou bloqueio não conseguirão ser encontrados, o que pode impactar se a solicitação será bloqueada ou permitida." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Not: Yeni bir bağlantı yapıldığında, bazen yalnızca IP adresi kullanılabilir olabilir. Bu tür durumlarda, izin verilenler veya engellenenler listesindeki etki alanları eşleştirilemez; bu, bir bağlantının engellenip engellenmeyeceğine etki edebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Примітка: Коли встановлюється нове з'єднання, іноді доступна лише IP-адреса. У таких випадках не можна зіставити домени зі списком дозволених або заблокованих, що може вплинути на те, чи буде запит заблоковано або дозволено." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نوٹ: جب ایک نیا کنکشن بنایا جاتا ہے، تو بعض اوقات صرف IP ایڈریس دستیاب ہوتا ہے۔ ایسے معاملات میں، اجازت یا بلاک لسٹ پر موجود ڈومینز کا مقابلہ نہیں کیا جا سکتا، جس سے یہ متاثر ہو سکتا ہے کہ درخواست بلاک کی جائے گی یا اجازت دی جائے گی۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "注意:建立新连接时,有时可能只有 IP 地址可用。在这种情况下,允许或阻止列表中的域名将无法匹配,这可能会影响请求是被阻止还是被允许。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "注意:當建立新的連線時,有時只有 IP 位址可用。在此情況下,允許或阻擋名單上的網域無法配對,這可能會影響連線請求是否被阻擋或允許。" } } } }, "7iJ-xR-xDI.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Local or remote file, containing endpoints to always allow.\"; ObjectID = \"7iJ-xR-xDI\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lokale oder externe Datei, die Endpunkte enthält, die immer erlaubt werden sollen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Local or remote file, containing endpoints to always allow." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Archivo local o remoto, que contiene endpoints para bloquear siempre. " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichier local ou distant contenant les points d’accès à toujours autoriser." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File locale o remoto contenente endpoint da consentire sempre." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항상 허용할 엔드포인트를 포함한 로컬 또는 원격 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Plik lokalny lub zdalny, zawierający punkty końcowe, na które zawsze należy zezwolić." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivo local ou remoto, contendo endpoints para sempre permitir." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yerel veya uzak dosya, her zaman izin verilecek içeren bitiş noktaları." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файл, що містить веб- або IP-адреси, які завжди дозволено." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مقامی یا ریموٹ فائل، جس میں ہمیشہ اجازت دینے والے اینڈ پوائنٹس شامل ہیں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "本地或远程文件,包含始终允许的端点。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "本地或遠端檔案,包含總是允許的端點。" } } } }, "7vn-ff-PJk.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Note: A profile defines a set of rules and settings. Once activated, its settings apply LuLu-wide (and can be modified via LuLu's Settings panes). Any new rules will be added only to that profile's ruleset.\"; ObjectID = \"7vn-ff-PJk\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinweis: Ein Profil definiert eine Reihe von Regeln und Einstellungen. Sobald es aktiviert ist, gelten diese Einstellungen für ganz LuLu (und können in den LuLu-Einstellungen geändert werden). Neue Regeln werden nur zu diesem Profil hinzugefügt." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Note: A profile defines a set of rules and settings. Once activated, its settings apply LuLu-wide (and can be modified via LuLu's Settings panes). Any new rules will be added only to that profile's ruleset." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nota: Un perfil define un conjunto de reglas y configuraciones. Una vez activado, sus configuraciones se aplican en todo LuLu (y se pueden modificar en los paneles de Configuración de LuLu). Cualquier regla nueva se añadirá solo al conjunto de reglas de ese perfil." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Remarque : un profil définit un ensemble de règles et de paramètres. Une fois activé, ses paramètres s’appliquent à l’ensemble de LuLu (et peuvent être modifiés dans les panneaux de paramètres de LuLu). Toute nouvelle règle sera ajoutée uniquement à l’ensemble de règles de ce profil." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nota: Un profilo definisce un insieme di regole e impostazioni. Una volta attivato, le sue impostazioni si applicano in tutto LuLu (e possono essere modificate nei pannelli Impostazioni di LuLu). Eventuali nuove regole verranno aggiunte solo al set di regole di quel profilo." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "참고: 프로필은 일련의 규칙과 설정을 정의합니다. 활성화되면 해당 설정이 LuLu 전체에 적용되며 (LuLu의 설정 창에서 수정할 수 있습니다). 새 규칙은 해당 프로필의 규칙 집합에만 추가됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uwaga: Profil definiuje zestaw reguł i ustawień. Po aktywowaniu ustawienia te mają zastosowanie w całym LuLu (i mogą być zmieniane w panelach ustawień LuLu). Nowe reguły będą dodawane tylko do zestawu reguł tego profilu." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nota: Um perfil define um conjunto de regras e configurações. Uma vez ativado, suas configurações são aplicadas em todo o LuLu (e podem ser modificadas nos painéis de Configurações do LuLu). Novas regras serão adicionadas somente ao conjunto de regras desse perfil." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Not: Bir profil, bir dizi kural ve ayar tanımlar. Etkinleştirildiğinde bu ayarlar LuLu genelinde geçerli olur (ve LuLu’nun Ayarlar pencerelerinden değiştirilebilir). Yeni kurallar yalnızca o profilin kural kümesine eklenir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Примітка: профіль визначає набір правил і налаштувань. Після активації його параметри застосовуються в LuLu (і можуть бути змінені через панелі налаштувань LuLu). Будь-які нові правила буде додано лише до набору правил цього профілю." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نوٹ: ایک پروفائل قواعد اور ترتیبات کا مجموعہ متعین کرتا ہے۔ فعال ہونے کے بعد، اس کی ترتیبات پوری LuLu پر لاگو ہوں گی (اور انہیں LuLu کی ترتیبات میں ترمیم کیا جا سکتا ہے)۔ کوئی بھی نیا قاعدہ صرف اسی پروفائل کے قواعد میں شامل کیا جائے گا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "注意:配置文件定义了一组规则和设置。激活后,其设置将在整个 LuLu 中生效(并可通过 LuLu 的“设置”面板进行修改)。任何新规则都只会添加到该配置文件的规则集中。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "注意:設定檔定義了一組規則與設定。啟用後,其設定將套用於整個 LuLu(並可透過 LuLu 的設定面板修改)。任何新的規則只會新增到該設定檔的規則集中。" } } } }, "9ts-yW-4uE.headerCell.title" : { "comment" : "Class = \"NSTableColumn\"; headerCell.title = \"Selected Profile\"; ObjectID = \"9ts-yW-4uE\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgewähltes Profil" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Selected Profile" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil seleccionado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil sélectionné" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo Selezionato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택된 프로필" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybrany profil" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil selecionado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçili Profil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибраний профіль" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "منتخب کردہ پروفائل" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已选择的配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已選取的設定檔" } } } }, "Bdp-Z8-BhB.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Local or remote file, containing endpoints to always block.\"; ObjectID = \"Bdp-Z8-BhB\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lokale oder externe Datei, die Endpunkte enthält, die immer blockiert werden sollen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Local or remote file, containing endpoints to always block." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Archivo local o remoto, que contiene endpoints para bloquear siempre. " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichier local ou distant contenant les points d’accès à toujours bloquer." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File locale o remoto contenente endpoint da bloccare sempre." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항상 차단할 엔드포인트를 포함한 로컬 또는 원격 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Plik lokalny lub zdalny, zawierający punkty końcowe, które należy zawsze blokować." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivo local ou remoto, contendo endpoints para sempre bloquear." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yerel veya uzak dosya, her zaman engellenecek içeren bitiş noktaları." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файл, що містить веб- або IP-адреси, які завжди блокуються." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مقامی یا ریموٹ فائل، جس میں ہمیشہ بلاک کرنے والے اینڈ پوائنٹس شامل ہیں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "本地或远程文件,包含始终要阻止的端点。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "本地或遠端檔案,包含總是阻擋的端點。" } } } }, "bYB-s1-S3f.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Run without showing an icon in the status menu bar.\"; ObjectID = \"bYB-s1-S3f\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausführen, ohne ein Statussymbol in der Menüleiste anzuzeigen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Run without showing an icon in the status menu bar." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutar sin mostrar un ícono en la barra de menú de estado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exécuter sans afficher d'icône dans la barre de menu." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File locale o remoto contenente endpoint da bloccare sempre." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "상태 메뉴 막대에 아이콘을 표시하지 않습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom bez wyświetlania ikony na pasku menu stanu." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Rodar sem mostrar ícone de staus na barra do menu." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Menü çubuğunda bir simge göstermeden çalıştırın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запускати без зображення іконки в панелі меню." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اسٹیٹس مینو بار میں آئیکن کو ظاہر کیے بغیر چلائیں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行时不在状态菜单栏中显示图标。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "執行時不於選單列中顯示圖示。" } } } }, "caA-Ec-ysj.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Outgoing connections to localhost will be allowed.\"; ObjectID = \"caA-Ec-ysj\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "Outgoing connections to localhost will be allowed." } } } }, "Cfj-2E-dIA.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"View Rules\"; ObjectID = \"Cfj-2E-dIA\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln anzeigen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "View Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kuralları Görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قواعد دیکھیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看規則" } } } }, "CqC-4v-5d3.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Allowed\"; ObjectID = \"CqC-4v-5d3\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allowed" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consentito" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dozwolone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitido" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволено" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اجازت دی گئی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已允许" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許" } } } }, "d79-kw-Ala.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"First create a name for your profile. \\n\\nThen click \\\"Next\\\" to configure the settings for your new profile.\"; ObjectID = \"d79-kw-Ala\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erstellen Sie zuerst einen Namen für Ihr Profil.\n\nKlicken Sie dann auf „Weiter“, um die Einstellungen für Ihr neues Profil zu konfigurieren." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "First create a name for your profile. \n\nThen click \"Next\" to configure the settings for your new profile." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Primero cree un nombre para su perfil.\n\nLuego haga clic en “Siguiente” para configurar los ajustes de su nuevo perfil." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Créez d’abord un nom pour votre profil.\n\nCliquez ensuite sur « Suivant » pour configurer les paramètres de votre nouveau profil." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Crea prima un nome per il tuo profilo. \n\nPoi fai clic su “Avanti” per configurare le impostazioni del tuo nuovo profilo." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "먼저 프로필 이름을 만드세요.\n\n그런 다음 \"다음\"을 클릭하여 새 프로필의 설정을 구성하세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Najpierw utwórz nazwę dla swojego profilu.\n\nNastępnie kliknij „Dalej”, aby skonfigurować ustawienia nowego profilu." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Primeiro crie um nome para o seu perfil.\n\nEm seguida, clique em “Avançar” para configurar as configurações do seu novo perfil." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önce profiliniz için bir ad oluşturun.\n\nArdından yeni profilinizin ayarlarını yapılandırmak için “İleri”ye tıklayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спочатку створіть назву для свого профілю.\n\nПотім натисніть «Далі», щоб налаштувати параметри для нового профілю." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "سب سے پہلے اپنے پروفائل کے لیے ایک نام بنائیں۔\n\nپھر \"اگلا\" پر کلک کریں تاکہ اپنے نئے پروفائل کی ترتیبات تشکیل دیں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "请先为您的配置文件创建一个名称。\n然后点击“下一步”来配置新配置文件的设置。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "請先為您的設定檔建立名稱。 \n\n然後點擊「下一步」以設定新設定檔的選項。" } } } }, "E0T-Ug-P8f.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Silently run without alerts, applying existing rules.\"; ObjectID = \"E0T-Ug-P8f\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Stille Ausführung ohne Mitteilungen unter Anwendung bestehender Regeln." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Silently run without alerts, applying existing rules." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutar silenciosamente sin alertas, aplicando reglas existentes." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exécution silencieuse sans alerte, en appliquant les règles existantes." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esegui in silenzio senza avvisi, applicando le regole esistenti." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "기존 규칙을 적용하고, 경고 없이 조용히 실행합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Działa w trybie cichym, bez alertów, stosując istniejące reguły." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Rodar silenciado sem alertas, aplicando regras existentes." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İkaz olmadan sessizce çalıştırın ve var olan kuralları uygulayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запускати у фоновому режимі без сповіщень і застосовувати наявні правила." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "الرٹس کے بغیر خاموشی سے چلائیں اور موجودہ قواعد کو لاگو کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "静默运行,不发出警报,应用现有规则。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "執行時不顯示警告,自動套用現有規則。" } } } }, "e5A-iH-fS4.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Yes\"; ObjectID = \"e5A-iH-fS4\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ja" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Yes" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sí" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Oui" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sì" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "예" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tak" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sim" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Evet" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Так" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ہاں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "是" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "是" } } } }, "e6s-dd-HG6.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Outgoing connections from programs running in the (iOS) simulator will be allowed.\"; ObjectID = \"e6s-dd-HG6\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Programme, welche in einem Simulator laufen, werden erlaubt." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Outgoing connections from programs running in the (iOS) simulator will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Se permitirán las aplicaciones que se ejecuten dentro del simulador." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Les applications fonctionnant dans le simulateur sont autorisées." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Le applicazioni in esecuzione nel simulatore saranno consentite." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시뮬레이터 내에서 실행되는 애플리케이션을 허용합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Aplikacje działające w symulatorze będą dozwolone." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Aplicações rodando no simulador serão permitidas." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Simülatörde çalışan uygulamalara izin verilecektir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Програми, що працюють у середовищі Симулятора, будуть дозволені." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "سیمولیٹر کے اندر چلنے والی ایپلیکیشنز کو اجازت دی جائے گی۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "将允许在模拟器内运行的应用程序。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "執行於模擬器內的應用程式將被允許" } } } }, "Emd-wS-UQh.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Block List:\"; ObjectID = \"Emd-wS-UQh\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockieren-Liste:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Block List:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Bloqueados:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Liste de blocage :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elenco Blocco:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단 리스트:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lista zablokowanych:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Bloqueio:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engel listesi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Список заблокованих:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک لسٹ:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止列表:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋名單" } } } }, "F0z-JX-Cv5.title" : { "comment" : "Class = \"NSWindow\"; title = \"Settings\"; ObjectID = \"F0z-JX-Cv5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einstellungen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Settings" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Configuración" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Paramètres" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impostazioni" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ustawienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Configurações" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayarlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Параметри" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ترتیبات" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定" } } } }, "F6g-hE-DiK.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Allow Localhost Traffic\"; ObjectID = \"F6g-hE-DiK\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "Allow Localhost Traffic" } } } }, "FE3-Li-d3K.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Allow List:\"; ObjectID = \"FE3-Li-d3K\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erlauben-Liste:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allow List:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Permitidos:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Liste d’autorisation :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elenco Consenti:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "허용 리스트:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lista dozwolonych:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Permissões:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin listesi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Список дозволених:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اجازت دی گئی لسٹ:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Allow List:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許名單" } } } }, "Fut-ad-zCW.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Browse\"; ObjectID = \"Fut-ad-zCW\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Durchsuchen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Browse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Navegar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Parcourir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sfoglia" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "찾아보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przeglądaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Buscar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Göz At" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "براؤز کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "浏览" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "瀏覽" } } } }, "GRx-gM-j7j.headerCell.title" : { "comment" : "Class = \"NSTableColumn\"; headerCell.title = \"Profile Name\"; ObjectID = \"GRx-gM-j7j\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profilname" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profile Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre del perfil" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom du profil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome Profilo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa profilu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome do perfil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва профілю" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "روفائل کا نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔名稱" } } } }, "GsR-9d-5N8.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"New connections should be be:\"; ObjectID = \"GsR-9d-5N8\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neue Verbindungen:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "New connections should be be:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Las nuevas conexiones deben ser:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les nouvelles connections doivent être :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le nuove connessioni dovrebbero essere:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 연결을 항상:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nowe połączenia powinny być:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Novas conexões devem ser:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni bağlantılara:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нові з'єднання мають бути:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نئی کنکشنز کو ہونا چاہیے:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新的连接应该是:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新的連線應被:" } } } }, "I4i-8n-Kep.placeholderString" : { "comment" : "Class = \"NSTextFieldCell\"; placeholderString = \"Profile Name\"; ObjectID = \"I4i-8n-Kep\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profilname" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profile Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre del perfil" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom du profil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome Profilo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa profilu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome do perfil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва профілю" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "روفائل کا نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔名稱" } } } }, "ICK-Xr-yt1.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Add Profile\"; ObjectID = \"ICK-Xr-yt1\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil hinzufügen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Add Profile" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar perfil" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un profil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi Profilo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj profil" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar perfil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати профіль" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "روفائل کا نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增設定檔" } } } }, "iOJ-Bc-hxG.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"No Icon Mode\"; ObjectID = \"iOJ-Bc-hxG\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kein-Statussymbol-Modus" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "No Icon Mode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo Sin Ícono" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode sans icône" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità senza icona" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "아이콘 비활성화 모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tryb bez ikony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo Sem Ícone" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Simgesiz kip" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Режим без іконки" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "آئیکن کے بغیر موڈ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无图标模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無圖示模式" } } } }, "iYm-DO-gHs.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"All outgoing UDP traffic on port 53 will be allowed.\"; ObjectID = \"iYm-DO-gHs\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Alle (UDP-)Verbindungen auf Port 53 werden erlaubt." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "All outgoing UDP traffic on port 53 will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Todo el tráfico (UDP) en el puerto 53 será permitido. " } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tout le trafic (UDP) sur le port 53 sera autorisé." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Tutto il traffico (UDP) sulla porta 53 sarà consentito." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "53번 포트를 통하는 모든 (UDP) 트래픽을 허용합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Cały ruch (UDP) na porcie 53 będzie dozwolony." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Todo tráfego (UDP) na porta 53 será permitido." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "53. kapıdaki tüm (UDP) trafiğe izin verilecektir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Увесь (UDP) трафік на порту 53 буде дозволено." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "پورٹ 53 پر تمام (UDP) ٹریفک کو اجازت دی جائے گی۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "允许端口 53 上的所有 (UDP) 流量。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "所有 53 連接埠的 (UDP)流量將被允許。" } } } }, "JV5-cY-ff4.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Do not automatically check for new versions.\"; ObjectID = \"JV5-cY-ff4\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht automatisch nach Updates suchen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Do not automatically check for new versions." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No verificar automáticamente nuevas versiones. " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ne pas rechercher automatiquement la présence de nouvelles versions." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non controllare automaticamente la disponibilità di nuove versioni." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동으로 새로운 버전을 확인하지 않습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie sprawdzaj automatycznie dostępności nowych wersji." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não verifique novas atualizações automaticamente." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni sürümleri kendiliğinden denetlemeyin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не перевіряти наявність нових версій автоматично." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نئی ورژنز کے لیے خودکار طور پر چیک نہ کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "不自动检查新版本。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "不自動檢查新版本。" } } } }, "JVI-Or-h0u.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Passive Mode\"; ObjectID = \"JVI-Or-h0u\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Passiver Modus" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Passive Mode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo Pasivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode passif" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità Passiva" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패시브 모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tryb pasywny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo Passivo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pasif kip" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пасивний режим" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پسیو موڈ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "被动模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "被動模式" } } } }, "jvT-uI-uNq.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Allow Already Installed Programs\"; ObjectID = \"jvT-uI-uNq\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Bereits installierte Programme erlauben." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allow Already Installed Programs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir Programas Instalados" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Autoriser les programmes installés" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Consenti Programmi Installati" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설치된 프로그램 허용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zezwalaj na zainstalowane programy" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir Programas Instalados" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kurulu programlara izin ver" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Дозволити установлені програми" } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "انسٹال شدہ پروگراموں کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "允许已安装的程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "允許已安裝程式" } } } }, "JwN-0v-5Uh.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"No\"; ObjectID = \"JwN-0v-5Uh\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nein" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "No" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Non" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "No" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "아니요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hayır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ні" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "否" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "否" } } } }, "k0w-Oi-kwd.label" : { "comment" : "Class = \"NSToolbarItem\"; label = \"Mode\"; ObjectID = \"k0w-Oi-kwd\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Modus" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Mode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tryb" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kip" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Режим" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "موڈ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "模式" } } } }, "k0w-Oi-kwd.paletteLabel" : { "comment" : "Class = \"NSToolbarItem\"; paletteLabel = \"Mode\"; ObjectID = \"k0w-Oi-kwd\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Modus" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Mode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tryb" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kip" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Режим" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "موڈ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "模式" } } } }, "kZA-kY-nGm.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Profile Name:\"; ObjectID = \"kZA-kY-nGm\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profilname" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Profile Name:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre del perfil:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom du profil :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome Profilo:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 이름:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa profilu:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome do perfil:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Adı:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва профілю:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "روفائل کا نام:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名称:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定檔名稱:" } } } }, "M8X-n4-v2d.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Outgoing connections from Apple-signed programs will be allowed.\"; ObjectID = \"M8X-n4-v2d\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Von Apple signierte Programme werden erlaubt." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Outgoing connections from Apple-signed programs will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Se permitirán los binarios firmados por Apple." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Les binaires signés par Apple seront autorisés." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "I binari firmati da Apple saranno consentiti." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Apple에 의해 서명된 실행 파일을 허용합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dozwolone będą pliki binarne podpisane przez Apple." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Binários assinados pela Apple serão permitidos." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Apple tarafından imzalanan ikililere izin verilecektir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Бінарні файли, підписані Apple, будуть дозволені." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "ایپل کے دستخط شدہ بائنریز کو اجازت دی جائے گی۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "将允许使用 Apple 签名的二进制文件。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "由 Apple 簽署的程式將被允許" } } } }, "mCn-AV-g9h.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Blocked\"; ObjectID = \"mCn-AV-g9h\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockieren" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Blocked" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bloqueado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bloqué" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Bloccato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zablokowany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloqueado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Заблоковано" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已阻止" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋" } } } }, "pGG-dq-qkU.label" : { "comment" : "Class = \"NSToolbarItem\"; label = \"Update\"; ObjectID = \"pGG-dq-qkU\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Update" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Modifier" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اپڈیٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新" } } } }, "pGG-dq-qkU.paletteLabel" : { "comment" : "Class = \"NSToolbarItem\"; paletteLabel = \"Update\"; ObjectID = \"pGG-dq-qkU\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Update" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Modifier" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اپڈیٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新" } } } }, "rrF-xj-cXp.label" : { "comment" : "Class = \"NSToolbarItem\"; label = \"Lists\"; ObjectID = \"rrF-xj-cXp\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Listen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Lists" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Listas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Listes" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elenchi" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "리스트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Listy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Listas" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Listeler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Списки" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "فہرستیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "名單" } } } }, "rrF-xj-cXp.paletteLabel" : { "comment" : "Class = \"NSToolbarItem\"; paletteLabel = \"Lists\"; ObjectID = \"rrF-xj-cXp\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Listen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Lists" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Listas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Listes" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elenchi" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "리스트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Listy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Listas" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Listeler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Списки" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "فہرستیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "名單" } } } }, "SG5-YM-BoV.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"All traffic routed thru LuLu will be blocked (unless the endpoint is explicitly on the 'Allow' list).\"; ObjectID = \"SG5-YM-BoV\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle über LuLu geleiteten Verbindungen werden blockiert (es sei denn, der Endpunkt ist ausdrücklich in der „Erlauben“-Liste aufgeführt)." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "All traffic routed thru LuLu will be blocked (unless the endpoint is explicitly on the 'Allow' list)." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todo el tráfico ruteado a través de LuLu será bloqueado (a menos que el endpoint esté explícito en la lista de 'Permitidos')." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout le trafic acheminé par LuLu sera bloqué (à moins que le point d’accès ne figure explicitement sur la liste 'Autoriser')." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutto il traffico instradato tramite LuLu verrà bloccato (a meno che l’endpoint non sia esplicitamente presente nell’elenco “Consenti”)." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu를 통하는 모든 트래픽을 차단합니다. (엔드포인트가 '허용' 리스트에 명시적으로 있지 않은 경우)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Cały ruch kierowany przez LuLu zostanie zablokowany (chyba że punkt końcowy znajduje się wyraźnie na liście „Zezwól”)." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Todo tráfego roteado pelo LuLu será bloqueado (a menos que o endpoint esteja explicitamente na lista de ‘Permissões’)." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu üzerinden aktarılan tüm trafik engellenir (bitiş noktası açıkça “İzin Ver” listesinde değilse)." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увесь трафік, що проходить через LuLu, буде заблоковано (якщо веб- або IP-адреса не в списку «Дозволених»)." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کے ذریعے روٹ کیا گیا تمام ٹریفک بلاک کر دیا جائے گا (جب تک کہ اینڈ پوائنٹ واضح طور پر 'اجازت' کی فہرست میں نہ ہو)۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "通过 LuLu 路由的所有流量都将被阻止(除非端点明确位于“允许”列表中)。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "所有經由 LuLu 路由的流量將被阻擋(除非該端點被列於允許名單中)。" } } } }, "Shu-Wz-4ax.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Allow Apple Programs\"; ObjectID = \"Shu-Wz-4ax\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apple-Programme erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allow Apple Programs" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir Programas de Apple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser les programmes Apple" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti Programmi Apple" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Apple 프로그램 허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwalaj na programy Apple" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir Programas Apple" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Apple programlarına izin ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити програми Apple" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایپل پروگراموں کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许 Apple 程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許 Apple 程式" } } } }, "Twx-TJ-y2Q.label" : { "comment" : "Class = \"NSToolbarItem\"; label = \"Rules\"; ObjectID = \"Twx-TJ-y2Q\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قواعد" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則" } } } }, "Twx-TJ-y2Q.paletteLabel" : { "comment" : "Class = \"NSToolbarItem\"; paletteLabel = \"Rules\"; ObjectID = \"Twx-TJ-y2Q\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قواعد" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則" } } } }, "Uqq-dW-xf2.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Allow Simulator Programs\"; ObjectID = \"Uqq-dW-xf2\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Simulator-Anwendungen erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allow Simulator Programs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir Aplicaciones del Simulador" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Autoriser les applications du simulateur" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Consenti Applicazioni del Simulatore" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시뮬레이터 애플리케이션 허용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zezwalaj na aplikacje symulacyjne" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir Aplicações do Simulador" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Simülatör uygulamalarına izin ver" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Дозволити програми Симулятора" } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "سیمولیٹر ایپلیکیشنز کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "允许模拟器应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "允許模擬器內的應用程式" } } } }, "W6X-IU-31r.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Browse\"; ObjectID = \"W6X-IU-31r\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Durchsuchen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Browse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Navegar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Parcourir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sfoglia" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "찾아보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przeglądaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Buscar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Göz At" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "براؤز کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "浏览" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "瀏覽" } } } }, "wED-Z0-197.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Allow DNS Traffic\"; ObjectID = \"wED-Z0-197\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "DNS-Verbindungen erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Allow DNS Traffic" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir Tráfico DNS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser le trafic DNS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti Traffico DNS" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "DNS 트래픽 허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwól na ruch DNS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir Tráfego DNS" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "DNS trafiğine izin ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити DNS трафік" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "DNS ٹریفک کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许 DNS 流量" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許 DNS 流量" } } } }, "wka-xk-z2g.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"No VirusTotal Mode\"; ObjectID = \"wka-xk-z2g\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kein-VirusTotal-Modus" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "No VirusTotal Mode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo Sin VirusTotal" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode sans VirusTotal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità senza VirusTotal" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "VirusTotal 비활성화 모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Bez trybu VirusTotal" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo sem vírusTotal" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "VirusTotal kipi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Режим без VirusTotal" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "وائرس ٹوٹل موڈ نہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无 VirusTotal 模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無 VirusTotal 模式" } } } }, "wVp-l1-Eac.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Next\"; ObjectID = \"wVp-l1-Eac\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profilname" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Next" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siguiente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suivant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avanti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dalej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Avançar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Далі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اگلا" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } } } }, "XTD-vf-hNE.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Disable integration with VirusTotal.\"; ObjectID = \"XTD-vf-hNE\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "VirusTotal-Integration deaktivieren." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Disable integration with VirusTotal." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Deshabilitar la integración con VirusTotal." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver l'intégration avec VirusTotal." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilita integrazione con VirusTotal." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "내장된 VirusTotal 연동 기능을 비활성화합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz integrację z VirusTotal." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desabilite a integração com o VirusTotal." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "VirusTotal ile entegrasyonu devre dışı bırakın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути інтеграцію з VirusTotal." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "وائرس ٹوٹل کے ساتھ انضمام کو غیر فعال کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用与 VirusTotal 的集成。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用與 VirusTotal 的整合功能。" } } } }, "z5b-fA-WCk.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Disable Update Checks\"; ObjectID = \"z5b-fA-WCk\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update-Prüfungen deaktivieren" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Disable Update Checks" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Deshabilitar Verificación de Actualizaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver la recherche de mise à jour" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilita Verifica Aggiornamenti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 확인 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz sprawdzanie aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desabilitar Verificação de Atualizações" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme denetimlerini devre dışı bırak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути перевірку оновлень" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اپڈیٹ چیکس کو غیر فعال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用更新检查" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用更新檢查" } } } }, "Zan-q8-ABi.title" : { "comment" : "Class = \"NSWindow\"; title = \"Add Profile\"; ObjectID = \"Zan-q8-ABi\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil hinzufügen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Add Profile" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar perfil" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un profil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi Profilo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj profil" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar perfil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати профіль" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل شامل کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增設定檔" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/Rules.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "2yS-Gr-1bI.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Label\"; ObjectID = \"2yS-Gr-1bI\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bezeichnung" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Label" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Etiqueta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Label" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Etichetta" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "레이블" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Etykieta" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Rótulo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etiket" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Мітка" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لیبل" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "标签" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "標籤" } } } }, "7zm-Uo-lwv.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Item Name\"; ObjectID = \"7zm-Uo-lwv\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Objektname" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Item Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre de Ítem" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom d’item" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome elemento" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항목 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa elementu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome do Item" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Öğe Adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва елемента" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "آئٹم کا نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "项目名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "項目名稱" } } } }, "b9m-wt-VZa.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Passively-created Rules\"; ObjectID = \"b9m-wt-VZa\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Passiv erstellte Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Passively-created Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas Pasivamente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles créées passivement" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole create passivamente" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "수동으로 생성된 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pasywnie utworzone reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras criadas passivamente" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pasif Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пасивно створені правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "حالیہ قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "被动创建的规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "被動建立的規則" } } } }, "cvb-ds-FcC.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"User-created Rules\"; ObjectID = \"cvb-ds-FcC\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vom Benutzer erstellte Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "User-created Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas del Usuario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles créées par l’utilisateur" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole create dall'utente" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자가 생성한 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły utworzone przez użytkownika" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras criadas pelo usuário" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kullanıcı Kuralları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Користувацькі правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "صارف کے قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "用户创建的规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "使用者建立的規則" } } } }, "DWG-ck-lDk.placeholderString" : { "comment" : "Class = \"NSSearchFieldCell\"; placeholderString = \"Filter Rules\"; ObjectID = \"DWG-ck-lDk\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln filtern" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Filter Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles de filtre" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Filtra regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 필터" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły filtrowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kuralları süz…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила фільтрації" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قوانین فلٹر کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "过滤规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "過濾規則" } } } }, "e4i-1F-tjh.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Recent Rules\"; ObjectID = \"e4i-1F-tjh\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neueste Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Recent Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas Recientes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles récentes" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole recenti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "최근 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ostatnie reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras recentes" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Son Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нещодавні правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "حالیہ قواعد" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "最近的规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "最近的規則" } } } }, "F0z-JX-Cv5.title" : { "comment" : "Class = \"NSWindow\"; title = \"LuLu Rules\"; ObjectID = \"F0z-JX-Cv5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu-Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas de LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu Kuralları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 規則" } } } }, "F7G-1t-415.label" : { "comment" : "Class = \"NSToolbarItem\"; label = \"Custom View\"; ObjectID = \"F7G-1t-415\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Benutzerdefinierte Ansicht" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Custom View" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vista Personalizada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vue personnalisée" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vista personalizzata" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자 지정 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Widok niestandardowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Vista Customizada" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Özel Görünüm" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Кастомний вигляд" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کاسٹم ویو" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自定义视图" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自訂畫面" } } } }, "F7G-1t-415.paletteLabel" : { "comment" : "Class = \"NSToolbarItem\"; paletteLabel = \"Custom View\"; ObjectID = \"F7G-1t-415\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Benutzerdefinierte Ansicht" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Custom View" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vista Personalizada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vue personnalisée" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vista personalizzata" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자 지정 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Widok niestandardowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Vista Customizada" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Özel Görünüm" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Кастомний вигляд" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کاسٹم ویو" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自定义视图" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自訂畫面" } } } }, "mQo-bf-aNx.headerCell.title" : { "comment" : "Class = \"NSTableColumn\"; headerCell.title = \" Rule\"; ObjectID = \"mQo-bf-aNx\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " Regel" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " Rule" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Regla" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règle" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regola" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : " 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguła" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regra" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правило" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قاعدہ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "規則" } } } }, "omZ-DL-6R7.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"All Rules\"; ObjectID = \"omZ-DL-6R7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "All Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todas las Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Toutes les règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutte le regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystkie reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Todas as Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Усі правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تمام قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "所有规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "所有規則" } } } }, "qbv-z3-4PI.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"item details...\"; ObjectID = \"qbv-z3-4PI\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Objektdetails..." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "item details..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Detalles de ítem..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détails de l'item" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "dettagli elemento..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항목 세부 정보..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "szczegóły elementu…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "detalhes sobre item…" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Öğe ayrıntıları…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "деталі елемента…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "آئٹم کی تفصیلات..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "项目详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "項目詳細資訊..." } } } }, "qNT-HP-XkH.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Apple Rules\"; ObjectID = \"qNT-HP-XkH\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apple-Regeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Apple Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas de Apple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles Apple" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole Apple" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Apple 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły Apple" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras da Apple" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Apple Kuralları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила Apple" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایپل قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Apple 规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Apple 規則" } } } }, "rEm-hF-r0T.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Default Rules\"; ObjectID = \"rEm-hF-r0T\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Standardregeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Default Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas Predeterminadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles par défaut" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole predefinite" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "기본 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły domyślne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras Padrão" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Saptanmış Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Стандартні правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ڈیفالٹ قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "默认规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "預設規則" } } } }, "tZf-9e-yHh.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"connection info\"; ObjectID = \"tZf-9e-yHh\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verbindungsinfo" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "connection info" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "información de conexión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "information de connection" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "informazioni connessione" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결 정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "informacje o połączeniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "informação sobre conexão" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "bağlantı bilgisi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "інформація про зʼєднання" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کنیکشن کا معلومات" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "连接信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "連線資訊" } } } }, "vD6-Bk-elz.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"(re)loading rules...\"; ObjectID = \"vD6-Bk-elz\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln (neu)laden..." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "(re)loading rules..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "(re)cargando reglas..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "(re)chargement des règles ..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "(ri)caricamento regole..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 (재)로드 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "(ponowne) ładowanie reguł…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "(re)carregando regras…" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "kurallar (yeniden) yükleniyor…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "(пере)завантаження правил..." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "(ری) لوڈنگ قوانین..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(重新)加载规则..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "(重新)載入規則..." } } } }, "wLE-Rk-GOf.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Add Rule\"; ObjectID = \"wLE-Rk-GOf\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regel hinzufügen" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Add Rule" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar Regla" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter règle" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi regola" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj regułę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Regra" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати правило" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قاعدہ شامل کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增規則" } } } }, "YzL-Xb-QI1.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"3rd-party Rules\"; ObjectID = \"YzL-Xb-QI1\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Drittanbieterregeln" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "3rd-party Rules" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas de terceros" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles tierce-partie" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole di terze parti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서드파티 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły stron trzecich" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras de Terceiros" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "3. Parti Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила від сторонніх постачальників" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تھرڈ پارٹی قوانین" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "第三方规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "第三方規則" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/StartupWindowController.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "tj8-5e-Vlc.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Now starting up...\"; ObjectID = \"tj8-5e-Vlc\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startet jetzt..." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Now starting up..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Iniciando ahora..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Démarrage en cours …" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvio in corso..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시동 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchamianie…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Inicializando…" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "şimdi başlatılıyor…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запуск…" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اب شروع ہو رہا ہے..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在启动..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在啟動..." } } } }, "XsZ-6a-7Rk.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Note: LuLu works best on macOS 15.3+, as Apple fixed bugs affecting network stability. \"; ObjectID = \"XsZ-6a-7Rk\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinweis: LuLu funktioniert am besten unter macOS 15.3+, da Apple Fehler behoben hat, die die Netzwerkstabilität beeinträchtigten." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Note: LuLu works best on macOS 15.3+, as Apple fixed bugs affecting network stability. " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nota: LuLu funciona mejor en macOS 15.3+, ya que Apple corrigió errores que afectaban la estabilidad de la red." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Remarque : LuLu fonctionne mieux sur macOS 15.3+, car Apple a corrigé des bugs affectant la stabilité du réseau." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nota: LuLu funziona al meglio su macOS 15.3+, poiché Apple ha corretto bug che influivano sulla stabilità della rete." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "참고: LuLu는 macOS 버전 15.3 이상에 최적화되었습니다. 이는 Apple이 네트워크 안정성에 영향을 주는 버그를 수정했기 때문입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uwaga: LuLu działa najlepiej na macOS 15.3+, ponieważ Apple naprawiło błędy wpływające na stabilność sieci." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nota: O LuLu funciona melhor no macOS 15.3+, pois a Apple corrigiu bugs que afetavam a estabilidade da rede." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Not: LuLu, macOS 15.3+ üzerinde en iyi şekilde çalışır, çünkü Apple ağ kararlılığını etkileyen hataları düzeltti." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Примітка: LuLu найкраще працює на macOS 15.3+, оскільки Apple виправила помилки, що впливали на стабільність мережі." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نوٹ: لولو macOS 15.3+ پر بہترین کام کرتا ہے، کیونکہ ایپل نے نیٹ ورک کی استحکام کو متاثر کرنے والے بگز کو ٹھیک کیا ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "注意:LuLu 在 macOS 15.3+ 上运行效果最佳,因为 Apple 修复了影响网络稳定性的错误。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "注意:由於 Apple 修復了影響網路穩定性的錯誤,LuLu 在 macOS 15.3+ 上運作最佳。" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/StatusBarPopover.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "2jG-uf-4XV.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"⌃\"; ObjectID = \"2jG-uf-4XV\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "⌃" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "^" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "^" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "⌃" } } }, "shouldTranslate" : false }, "17.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"LuLu up & running!\"; ObjectID = \"17\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu läuft!" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu up & running!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡LuLu en funcionamiento!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu est opérationnel !" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LuLu è attivo e funzionante!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 정상 작동 중!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu jest uruchomiona!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu está rodando!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu açık ve çalışıyor!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu запущено і працює!" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "!لولو چل رہا ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 已启动并运行中!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 已啟動並執行中!" } } } }, "22.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Access it from here anytime.\"; ObjectID = \"22\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Greife jederzeit von hier aus darauf zu." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Access it from here anytime." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acceda desde aquí en cualquier momento." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Accédez-y ici à tout moment" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Accedici da qui in qualsiasi momento." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "언제든 여기서 이용할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dostęp możesz uzyskać w dowolnym momencie stąd." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Acesse-o daqui quando quiser." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ona buradan istediğiniz zaman erişebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Отримуйте доступ до нього звідси в будь-який час." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کسی بھی وقت یہاں سے اس تک رسائی حاصل کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可随时从这里访问。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隨時可以從這裡存取。" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/UpdateWindow.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "F0z-JX-Cv5.title" : { "comment" : "Class = \"NSWindow\"; title = \"LuLu Update\"; ObjectID = \"F0z-JX-Cv5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu Update" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu Update" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour de LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu’yu Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو اپڈیٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 更新" } } } }, "nMV-1f-RyK.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Update\"; ObjectID = \"nMV-1f-RyK\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Update" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atializar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اپڈیٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/mul.lproj/Welcome.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "3aO-AI-YtY.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Outgoing connections from currently installed 3rd-party programs will be allowed.\"; ObjectID = \"3aO-AI-YtY\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Diese Option erlaub allen bereits installierten Drittanbieterprogrammen auf das Netzwerk zugreifen, ohne eine Mitteilung auszulösen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Outgoing connections from currently installed 3rd-party programs will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Esta opción permitirá que cualquier aplicación de terceros que ya esté instalada acceda a la red sin generar una alerta." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Cette option permet à toute application tierce déjà installée d'accéder au réseau sans générer d'alerte." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Questa opzione consentirà a qualsiasi app di terze parti già installata di accedere alla rete senza generare un avviso." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이 옵션은 이전에 설치한 모든 타사 프로그램을 허용합니다. 해당 프로그램은 경고 발생 없이 네트워크에 접근할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Opcja ta umożliwi każdej zainstalowanej już aplikacji innej firmy dostęp do sieci bez generowania alertu." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Esta opção permitirá que qualquer aplicativo de terceiros que já esteja instalado tenha acesso à rede sem gerar um alerta." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bu seçenek, halihazırda kurulu olan herhangi bir 3. parti uygulamanın bir ikâz oluşturmadan ağa erişimine izin verir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Ця опція дозволить будь-якому сторонньому додатку, який уже встановлено, отримувати доступ до мережі без сповіщення." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "یہ آپشن کسی بھی پہلے سے انسٹال شدہ تھرڈ پارٹی ایپ کو الرٹ پیدا کیے بغیر نیٹ ورک تک رسائی کی اجازت دے گا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "此选项将允许任何已安装的第三方应用程序访问网络而不会生成警报。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "這個選項將允許任何已安裝的第三方應用程式存取網路,而不會產生警告。" } } } }, "7pD-BH-c8i.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Mahalo to the \\\"Friends Objective-See\\\"\"; ObjectID = \"7pD-BH-c8i\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mahalo an die \"Friends Objective-See\"" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Mahalo to the \"Friends Objective-See\"" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Gracias a los \"Friends of Objective-See\"" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mahalo aux « Amis Objectif-See »" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Grazie agli \"Amici di Objective-See\"" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "\"Objective-See 친구들\"에게, 마할로!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Mahalo do „Przyjaciół Objective-See”" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mahalo para os “Amigos Objective-See\"" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "“Objective-See” Arkadaşlarına Mahalo" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дякуємо «Friends Objective-See»" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "\"Objective-See دوستوں\" کو شکریہ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "感谢“Objective-See 的伙伴们”" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "感謝「Objective-See 的朋友」" } } } }, "8KH-RD-y9L.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Note: steps may vary by macOS version\"; ObjectID = \"8KH-RD-y9L\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "Note: steps may vary by macOS version" } } } }, "C1V-iy-oEo.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"1. In the \\\"System Extension Blocked\\\" alert, click: \\\"Open System Settings\\\"\"; ObjectID = \"C1V-iy-oEo\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "1. Auf der „Systemerweiterung blockiert“-Meldung drücke: „Systemeinstellungen öffnen“." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "1. In the \"System Extension Blocked\" alert, click: \"Open System Settings\"" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "1. En la alerta \"Extensión de Sistema Bloqueada\", haga click en: \"Abrir Configuración del Sistema\"" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "1. Dans l'alerte « Extension du système bloquée », cliquez sur : « Ouvrir les Réglages Système »" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "1. Nell’avviso \"Estensione di sistema bloccata\", fai clic su: \"Apri Impostazioni di sistema\"" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "1. \"새로운 네트워크 확장 프로그램\" 경고 대화상자에서 \"시스템 설정 열기\"를 클릭합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "1. W alercie „Zablokowane rozszerzenie systemu” kliknij: „Otwórz ustawienia systemu”" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "1. No alerta de “Extensão do Sistema Bloqueada”, clique em “Abrir Preferências do Sistema”" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "1. “Sistem Genişletmesi Engellendi” ikâzında “Sistem Ayarları’nı Aç”a tıklayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "1. У сповіщенні «Системне розширення заблоковано» натисніть «Відкрити системні параметри»." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "1. \"سسٹم ایکسٹینشن بلاک\" الرٹ میں، کلک کریں: \"سسٹم سیٹنگز کھولیں\"" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "1. 在“系统扩展被阻止”警报中,单击:“打开系统设置”" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "1. 在「已封鎖的系統延伸功能」的警示中,按下「開啟系統設定」" } } } }, "Cwe-NI-HiJ.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"First, allow the LuLu Network Extension:\"; ObjectID = \"Cwe-NI-HiJ\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zuerst muss die LuLu Netzwerkerweiterung zugelassen werden:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "First, allow the LuLu Network Extension:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Primero, permite la Extensión de Red de LuLu:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout d'abord, autorisez l'extension de réseau LuLu :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Per prima cosa, consenti l’Estensione di rete di LuLu:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "먼저, LuLu 네트워크 확장 프로그램을 허용합니다:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Najpierw zezwól na rozszerzenie sieciowe LuLu:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Primeiro, dê permissão à Extensão de Rede LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Öncelikle, LuLu ağ genişletmesine izin verin:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спочатку, надайте доступ розширенню LuLu:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پہلے، LuLu نیٹ ورک ایکسٹینشن کو اجازت دیں:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "首先,允许 LuLu 网络扩展:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "首先,允許 LuLu 網路延伸功能:" } } } }, "eic-lg-Ln7.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"It monitors network activity and alerts you when a program first tries to initiate an outgoing connection, allowing you to permit or deny it.\"; ObjectID = \"eic-lg-Ln7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Überwacht die Netzwerkaktivität und warnt, wenn ein Programm zum ersten Mal versucht, eine ausgehende Verbindung herzustellen, sodass diese erlaubt oder blockiert werden kann." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "It monitors network activity and alerts you when a program first tries to initiate an outgoing connection, allowing you to permit or deny it." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Monitorea la actividad de la red y alerta cuando un programa intenta iniciar por primera vez una conexión saliente, permitiéndote permitirla o denegarla. " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Il surveille l'activité du réseau et vous avertit lorsqu'un programme tente pour la première fois d'établir une connexion sortante, ce qui vous permet de l'autoriser ou de la refuser." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Per prima cosa, consenti l’Estensione di rete di LuLu:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "네트워크 활동을 모니터하고, 어떤 프로그램이 나가는 연결을 최초로 시도할 때 사용자가 허용하거나 거부할 수 있도록 경고합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Monitoruje aktywność sieciową i powiadamia Cię, gdy program po raz pierwszy próbuje zainicjować połączenie wychodzące, umożliwiając Ci zezwolenie na nie lub jego zablokowanie." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ele monitora a atividade na rede e te alerta quando um programa tenta iniciar uma conexão de saída, lhe permitindo que possa autorizá-la ou não." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ağ etkinliğinizi izler ve bir program ilk kez dışarıya bir bağlantı sağlamaya çalıştığında sizi ikâz eder ve izin vermenizi veya engellemenizi ister." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вона відстежує мережеву активність і сповіщає вас, коли програма вперше намагається ініціювати вихідне з'єднання, тож ви зможете його дозволити або заборонити." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "یہ نیٹ ورک کی سرگرمیوں کو مانیٹر کرتا ہے اور آپ کو الرٹ کرتا ہے جب کوئی پروگرام پہلی بار باہر کی طرف کنیکشن قائم کرنے کی کوشش کرتا ہے، آپ کو اجازت دینے یا انکار کرنے کی اجازت دیتا ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "它监视网络活动,并在程序首次尝试启动传出连接时发出警报,以便您允许或拒绝它。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "當程式首次嘗試建立對外連線時,它會監控網路活動並向您發出警告,讓您可以允許或拒絕連線。" } } } }, "EOD-Hh-8e5.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"...\"; ObjectID = \"EOD-Hh-8e5\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "…" } } }, "shouldTranslate" : false }, "F0z-JX-Cv5.title" : { "comment" : "Class = \"NSWindow\"; title = \"LuLu\"; ObjectID = \"F0z-JX-Cv5\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu" } } }, "shouldTranslate" : false }, "NCu-fH-K04.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"These can be changed later, via LuLu's Settings.\"; ObjectID = \"NCu-fH-K04\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "These can be changed later, via LuLu's Settings." } } } }, "pMo-Yj-XKY.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"LuLu is a free open-source firewall for macOS.\"; ObjectID = \"pMo-Yj-XKY\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lulu ist eine freie quelloffene Firewall für macOS" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "LuLu is a free open-source firewall for macOS." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu es un firewall gratuito y open-source para macOS." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu est un pare-feu libre et gratuit pour macOS." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LuLu è un firewall gratuito e open-source per macOS." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu는 macOS를 위한 무료 오픈소스 방화벽입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu to darmowa zapora sieciowa typu open source dla systemu macOS." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu é um firewall gratuito e open-source para macOS." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu, macOS için ücretsiz ve açık kaynak bir güvenlik duvarıdır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu — це безкоштовний фаєрвол із відкритим кодом для macOS." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu ایک مفت اوپن سورس فائر وال ہے جو macOS کے لیے ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 是一款适用于 macOS 的免费开源防火墙。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 是一款用於 macOS 的免費開源防火牆。" } } } }, "QA3-Ww-PeC.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"No\"; ObjectID = \"QA3-Ww-PeC\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nein" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "No" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Non" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "No" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "아니요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hayır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ні" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "否" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "否" } } } }, "qNj-Zh-8FW.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Support LuLu?\"; ObjectID = \"qNj-Zh-8FW\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu unterstützen?" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Support LuLu?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Apoyas a LuLu?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Soutenir LuLu ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vuoi supportare LuLu?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu를 응원하시겠어요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wesprze\u0003ć LuLu?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Apoiar o LuLu?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu’yu desteklemek ister misiniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримати LuLu?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کیا آپ LuLu کی مدد کرنا چاہتے ہیں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "支持LuLu吗?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "支持 LuLu?" } } } }, "qYE-G3-j1K.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"4. In the \\\"Filter Network Content\\\" alert, click: \\\"Allow\\\"\"; ObjectID = \"qYE-G3-j1K\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "4. Klicke in dem Hinweis „Netzwerkinhalte filtern“ auf: „Erlauben“" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "4. In the \"Filter Network Content\" alert, click: \"Allow\"" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "4. En la alerta \"Filtrar Contenido de Red\", haga click en: \"Permitir\"" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "4. Dans l'alerte « Filtrer le contenu du réseau », cliquez sur : « Autoriser »" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "4. Nell’avviso \"Filtra contenuti di rete\", fai clic su: \"Consenti\"" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "4. \"네트워크 콘텐츠 필터링\" 경고 대화상자에서 \"허용\"을 클릭합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "4. W alercie „Filtruj zawartość sieciową” kliknij: „Zezwól”" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "4. No alerta de “Filtrar Conteúdo de Rede”, clique em “Permitir”" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "4. “Ağ İçeriğini Filtrele” ikâzında “İzin Ver”e tıklayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "4. У сповіщенні «Фільтрувати мережевий контент» натисніть «Дозволити»." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "4. \"نیٹ ورک مواد کو فلٹر کریں\" الرٹ میں، کلک کریں: \"اجازت دیں\"" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "4. 在“过滤网络内容”警告中,点击“允许”" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "4. 在「過濾網路內容」的警告中,按下「允許」" } } } }, "Qyh-uK-Afv.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Welcome to\"; ObjectID = \"Qyh-uK-Afv\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Willkommen zu" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Welcome to" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bienvenido a " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bienvenue sur" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Benvenuto in" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "반가워요!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Witamy w" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bem-vindo ao" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hoş Geldiniz" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ласкаво просимо до" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خوش آمدید" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "欢迎使用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "歡迎使用" } } } }, "r1b-Nl-Ii7.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Outgoing connections from Apple-signed programs will be allowed.\"; ObjectID = \"r1b-Nl-Ii7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Diese Option erlaubt allen von Apple signierten Programmen auf das Netzwerk zuzugreifen, ohne eine Mitteilung auszulösen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Outgoing connections from Apple-signed programs will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Esta opción permitirá que cualquier código binario firmado por Apple acceda a la red sin generar una alerta." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Cette option permet à tout binaire signé par Apple d'accéder au réseau sans générer d'alerte." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Questa opzione consentirà a qualsiasi binario firmato da Apple di accedere alla rete senza generare un avviso." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이 옵션을 활성화하면 Apple에 의해 서명된 실행 파일을 허용합니다. 해당 프로그램은 경고 발생 없이 네트워크에 접근할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ta opcja umożliwi dostęp do sieci każdemu plikowi binarnemu podpisanemu przez Apple bez generowania alertu." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Esta permissão permitirá que qualquer binário assinado pela Apple tenha acesso à rede sem gerar um alerta." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bu seçenek, Apple tarafından imzalanmış herhangi bir ikilinin, bir ikâz oluşturmadan ağa erişimine izin verecektir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Ця опція дозволить будь-якому бінарному файлу, підписаному Apple, отримувати доступ до мережі без сповіщення." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "یہ آپشن کسی بھی ایپل سائنڈ بائنری کو الرٹ پیدا کیے بغیر نیٹ ورک تک رسائی کی اجازت دے گا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "此选项将允许任何 Apple 签名的二进制文件访问网络而不会生成警报。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "此選項將允許任何由 Apple 簽署的程式存取網路,而不會產生警告。" } } } }, "Re5-aD-QVR.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Yes!\"; ObjectID = \"Re5-aD-QVR\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ja!" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Yes!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Sí!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Oui !" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sì!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "좋아요!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tak!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sim!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Evet!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Так!" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "جی!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "是!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "是!" } } } }, "sAE-Mf-3fz.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"All outgoing UDP traffic on port 53 will be allowed.\"; ObjectID = \"sAE-Mf-3fz\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Diese Option erlaubt alle (UDP-) Verbindungen auf Port 53." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "All outgoing UDP traffic on port 53 will be allowed." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Esta opción permitirá cualquier tráfico (UDP) sobre el puerto 53. " } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Cette option autorise tout trafic (UDP) sur le port 53." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Questa opzione consentirà qualsiasi traffico (UDP) sulla porta 53." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이 옵션을 활성화하면 53번 포트를 통하는 모든 (UDP) 트래픽을 허용합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Opcja ta zezwoli na cały ruch (UDP) przez port 53." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Esta opção permitirá qualquer tráfego (UDP) na porta 53." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bu seçenek, 53. kapı üzerinden herhangi bir UDP trafiğine izin verecektir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Ця опція дозволить будь-який (UDP) трафік через порт 53." } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "یہ آپشن کسی بھی (UDP) ٹریفک کو پورٹ 53 پر اجازت دے گا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "此选项将允许通过端口 53 的任何 (UDP) 流量。" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "此選項將允許任何連接埠 53 的(UDP)流量。" } } } }, "SWc-iv-oN4.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \" Allow DNS Traffic\"; ObjectID = \"SWc-iv-oN4\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " DNS-Verbindungen erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " Allow DNS Traffic" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir Tráfico DNS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser le trafic DNS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti traffico DNS" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : " DNS 트래픽 허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwól na ruch DNS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir tráfego de DNS" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "DNS trafiğine izin ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити DNS трафік" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "DNS ٹریفک کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许 DNS 流量" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許 DNS 流量" } } } }, "Uzd-7P-dzG.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"It's free, open-source, and written by a single (Mac-loving) coder!\"; ObjectID = \"Uzd-7P-dzG\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Es ist frei, quelloffen und von einem einzigen (Mac-liebenden) Entwickler geschrieben! " } }, "en" : { "stringUnit" : { "state" : "new", "value" : "It's free, open-source, and written by a single (Mac-loving) coder!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin cargo, open-source y desarrollado por un único programador (amante de Mac)! " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "C’est gratuit, open-source et écrit par un seul codeur (amoureux du Mac) !" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "È gratuito, open-source e scritto da un singolo programmatore (innamorato del Mac)!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "무료이면서, 오픈소스에, 한 (Mac 덕후) 코더에 의해 개발되었습니다!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Jest darmowy, ma otwarte oprogramowanie i został napisany przez jednego (miłośnika Maców) programistę!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "É gratuito, open-source, e criado por um único programador (amante de Mac)!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ücretsizdir, açık kaynaktır ve tek bir (Mac hastası) kodcu tarafından yazılmıştır!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Безкоштовна, з відкритим кодом, створена однією людиною." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "یہ مفت ہے، اوپن سورس ہے اور ایک ہی (میک لوونگ) کوڈر نے لکھا ہے!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "它是免费的、开源的,并且是由一位(热爱 Mac 的)程序员编写的!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這是一款免費、開源的軟體,由一位(熱愛 Mac的)程式設計師所開發!" } } } }, "VIe-9o-Kc7.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Waiting for System Extension Approval\"; ObjectID = \"VIe-9o-Kc7\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Warten auf die Genehmigung der Systemerweiterung" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Waiting for System Extension Approval" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esperando Aprobación de la Extensión del Sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En attente de l'approbation de l'extension du système" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In attesa dell’approvazione dell’estensione di sistema" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템 확장 프로그램 승인 대기 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Oczekiwanie na zatwierdzenie rozszerzenia systemu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Esperando aprovação da extensão do sistema" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sistem Genişletmesi Onayı Bekleniyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очікується схвалення системного розширення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "سیسٹم ایکسٹینشن کی منظوری کا انتظار" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "等待系统扩展批准" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在等待系統延伸功能被核准" } } } }, "XBE-gW-YkK.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Show some love? \"; ObjectID = \"XBE-gW-YkK\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ein bisschen Liebe zeigen?" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Show some love? " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Nos das tu apoyo?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Montrer un peu d’amour ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vuoi mostrare un po’ di supporto?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애정을 보여주실래요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Okażesz trochę miłości?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dar carinho?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Biraz sevgi gösterseniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Бажаєте підтримати?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کیا آپ ہمیں پیار دکھانا چاہتے ہیں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "表达一些支持?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "要表達一些支持嗎?" } } } }, "xbU-XH-hYT.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Next\"; ObjectID = \"xbU-XH-hYT\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weiter" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Next" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siguiente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suivant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avanti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dalej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Próximo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sonraki" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Далі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اگلا" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } } } }, "XHD-XH-wR0.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"Now, let's configure LuLu:\"; ObjectID = \"XHD-XH-wR0\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nun wollen wir LuLu konfigurieren:" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Now, let's configure LuLu:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ahora, configuremos LuLu:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Maintenant, configurons LuLu :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ora configuriamo LuLu:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자, LuLu를 구성해봅시다:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Teraz skonfigurujmy LuLu:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Agora vamos configurar o LuLu:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Haydi LuLu’yu yapılandıralım:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "А тепер давайте налаштуємо LuLu:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اب، ہم LuLu کو تشکیل دیں:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "现在,让我们配置 LuLu:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "現在,讓我們來設定 LuLu:" } } } }, "ymJ-tB-qiA.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \" Allow Already Installed Programs\"; ObjectID = \"ymJ-tB-qiA\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "needs_review", "value" : " Bereits installierte Anwendungen erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " Allow Already Installed Programs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir Aplicaciones Ya Instaladas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Autoriser les applications déjà installées" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Consenti applicazioni già installate" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : " 사전에 설치된 프로그램 허용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zezwalaj na już zainstalowane aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir aplicações já instaladas" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Halihazırda kurulu uygulamalara izin ver" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Дозволити вже установлені програми" } }, "ur" : { "stringUnit" : { "state" : "needs_review", "value" : "پہلے سے انسٹال شدہ ایپلی کیشنز کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "允许已安装的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "允許已安裝的應用程式" } } } }, "YXa-F3-XSG.title" : { "comment" : "Class = \"NSButtonCell\"; title = \"Next\"; ObjectID = \"YXa-F3-XSG\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weiter" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Next" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siguiente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suivant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avanti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dalej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Próximo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sonraki" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Далі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اگلا" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } } } }, "zo1-a0-tPg.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \" Allow Apple Programs\"; ObjectID = \"zo1-a0-tPg\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " Apple-Programme erlauben" } }, "en" : { "stringUnit" : { "state" : "new", "value" : " Allow Apple Programs" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir Programas de Apple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser les programmes Apple" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti programmi Apple" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : " Apple 프로그램 허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwalaj na programy Apple" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir programas Apple" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Apple programlarına izin ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити програми Apple" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایپل پروگرامز کو اجازت دیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许 Apple 程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許 Apple 程式" } } } }, "ZZK-2G-iIB.title" : { "comment" : "Class = \"NSTextFieldCell\"; title = \"3. Authenticate\"; ObjectID = \"ZZK-2G-iIB\";", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "3. Authentifiziere" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "3. Authenticate" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "3. Autenticar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "3. S’authentifier" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Autenticati" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "3. 인증합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "3. Uwierzytelnij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "3. Autenticar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "3. Yetkilendirin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "3. Аутентифікувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "3. تصدیق کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "3. 身份验证" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "3. 進行驗證" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/App/patrons.txt ================================================ Patrons (2^6+): Jan Koum, Matt Mullenweg, Christian Blümlein, Shain Singh, Andreas Fink, Nuno, Rabi Rob Thomas, Mikhail S. Friends of Objective-See: Kandji, Fleet, Jamf, Moonlock, Palo Alto Networks, Sophos, Malwarebytes, iVerify, Huntress, SmugMug, Guardian Mobile Firewall, Halo Privacy, The Mitten Mac ================================================ FILE: LuLu/Extension/Alerts.h ================================================ // // file: Alerts.h // project: lulu (launch daemon) // description: alert related logic/tracking (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import OSLog; @import Foundation; @import NetworkExtension; #import "Process.h" #import "XPCUserProto.h" #import "XPCUserClient.h" @interface Alerts : NSObject /* PROPERTIES */ //shown alerts @property(nonatomic, retain)NSMutableDictionary* shownAlerts; //xpc client for talking to user (login item) @property(nonatomic, retain)XPCUserClient* xpcUserClient; //console user @property(nonatomic, retain)NSString* consoleUser; /* METHODS */ //create an alert object -(NSMutableDictionary*)create:(NEFilterSocketFlow*)flow process:(Process*)process; //via XPC, send an alert -(BOOL)deliver:(NSDictionary*)alert reply:(void (^)(NSDictionary*))reply; //is related to a shown alert? // checks if path/signing info is same -(BOOL)isRelated:(Process*)process; //add an alert to 'shown' -(void)addShown:(NSDictionary*)alert; //remove an alert from 'shown' -(void)removeShown:(NSDictionary*)alert; @end ================================================ FILE: LuLu/Extension/Alerts.m ================================================ // // file: Alerts.m // project: lulu (launch daemon) // description: alert related logic/tracking // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "Process.h" #import "Alerts.h" #import "utilities.h" /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation Alerts @synthesize shownAlerts; @synthesize consoleUser; @synthesize xpcUserClient; //init -(id)init { //super self = [super init]; if(nil != self) { //alloc shown shownAlerts = [NSMutableDictionary dictionary]; //init user xpc client xpcUserClient = [[XPCUserClient alloc] init]; } return self; } //create an alert dictionary -(NSMutableDictionary*)create:(NEFilterSocketFlow*)flow process:(Process*)process { //event for alert NSMutableDictionary* alert = nil; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //alloc alert = [NSMutableDictionary dictionary]; //add uuid alert[KEY_UUID] = [[NSUUID UUID] UUIDString]; //add key alert[KEY_KEY] = process.key; //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)flow.remoteEndpoint; //add pid alert[KEY_PROCESS_ID] = [NSNumber numberWithUnsignedInt:process.pid]; //add args if(0 != process.arguments.count) { //add alert[KEY_PROCESS_ARGS] = process.arguments; } //add path alert[KEY_PATH] = process.path; //add name alert[KEY_PROCESS_NAME] = process.name; //add process state if(YES == process.deleted) { //add alert[KEY_PROCESS_DELETED] = @YES; } //process ancestors? // ...only add if more than just self if(process.ancestors.count > 1) { //add alert[KEY_PROCESS_ANCESTORS] = process.ancestors; } //add (remote) ip alert[KEY_HOST] = remoteEndpoint.hostname; //add (remote) host // as string though, since XPC doesn't like NSURLs if(nil != flow.URL) { //add alert[KEY_URL] = flow.URL.absoluteString; } //add (remote) port alert[KEY_ENDPOINT_PORT] = remoteEndpoint.port; //add protocol alert[KEY_PROTOCOL] = [NSNumber numberWithInt:flow.socketProtocol]; //add signing info if(nil != process.csInfo) { //add alert[KEY_CS_INFO] = process.csInfo; } return alert; } //is related to a shown alert? // checks if path/signing info is same -(BOOL)isRelated:(Process*)process { //flag __block BOOL related = NO; //alert NSDictionary* alert = nil; //sync @synchronized(self.shownAlerts) { //grab alert // none, means its new alert = self.shownAlerts[process.key]; if(nil == alert) { //bail goto bail; } //related related = YES; }//sync bail: return related; } //add an alert to 'shown' -(void)addShown:(NSDictionary*)alert { //dbg msg os_log_debug(logHandle, "adding alert to 'shown': %{public}@ -> %{public}@", alert[KEY_KEY], alert); //add alert @synchronized(self.shownAlerts) { //add self.shownAlerts[alert[KEY_KEY]] = alert; } return; } //remove an alert from 'shown' -(void)removeShown:(NSDictionary*)alert { //dbg msg os_log_debug(logHandle, "removing alert from 'shown': %{public}@ -> %{public}@", alert[KEY_KEY], alert); //remove alert @synchronized(self.shownAlerts) { //remove [self.shownAlerts removeObjectForKey:alert[KEY_KEY]]; } return; } //via XPC, send an alert to the client (user) -(BOOL)deliver:(NSDictionary*)alert reply:(void (^)(NSDictionary*))reply { //flag BOOL delivered = NO; //dbg msg os_log_debug(logHandle, "delivering alert %{public}@", alert); //send via XPC to user if(YES != (delivered = [self.xpcUserClient deliverAlert:alert reply:reply])) { //err msg os_log_error(logHandle, "ERROR: failed to deliver alert to user (no client?)"); //bail goto bail; } bail: return delivered; } @end ================================================ FILE: LuLu/Extension/Binary.h ================================================ // // Binary.h // LuLu // // Created by Patrick Wardle on 8/27/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #ifndef Binary_h #define Binary_h #import "Consts.h" #import "signing.h" #import "utilities.h" @import OSLog; @import CommonCrypto; @interface Binary : NSObject { } /* PROPERTIES */ //path @property(nonatomic, retain)NSString* _Nonnull path; //name @property(nonatomic, retain)NSString* _Nonnull name; //icon @property(nonatomic, retain)NSImage* _Nonnull icon; //file attributes @property(nonatomic, retain)NSDictionary* _Nullable attributes; //spotlight meta data @property(nonatomic, retain)NSDictionary* _Nullable metadata; //bundle // nil for non-apps @property(nonatomic, retain)NSBundle* _Nullable bundle; //signing info @property(nonatomic, retain)NSMutableDictionary* _Nonnull csInfo; //hash @property(nonatomic, retain)NSMutableString* _Nonnull sha256; /* METHODS */ //init w/ a path -(id _Nonnull)init:(NSString* _Nonnull)path; /* the following methods are rather CPU-intensive as such, if the proc monitoring is run with the 'goEasy' option, they aren't automatically invoked */ //get an icon for a process // for apps, this will be app's icon, otherwise just a standard system one -(void)getIcon; //generate signing info (statically) -(void)generateSigningInfo:(SecCSFlags)flags; @end #endif /* Binary_h */ ================================================ FILE: LuLu/Extension/Binary.m ================================================ // // File: Binary.m // Project: Lulu // // Created by: Patrick Wardle // Copyright: 2017 Objective-See // #import "Binary.h" @implementation Binary @synthesize icon; @synthesize name; @synthesize path; @synthesize bundle; @synthesize metadata; @synthesize attributes; /* GLOBALS */ //log handle extern os_log_t logHandle; //init binary object // note: CPU-intensive logic (code signing, etc) called manually -(id)init:(NSString*)binaryPath { //init super self = [super init]; if(nil != self) { //save path self.path = binaryPath; //remove symlinks self.path = [self.path stringByResolvingSymlinksInPath]; //try load app bundle // will be nil for non-apps [self getBundle]; //get name [self getName]; //get file attributes [self getAttributes]; //get meta data (spotlight) [self getMetadata]; } return self; } //try load app bundle // will be nil for non-apps -(void)getBundle { //first try just with path self.bundle = [NSBundle bundleWithPath:path]; //that failed? // try find it dynamically if(nil == self.bundle) { //find bundle self.bundle = findAppBundle(path); } return; } //figure out binary's name // either via app bundle, or from path -(void)getName { //first try get name from app bundle // specifically, via grab name from 'CFBundleName' if(nil != self.bundle) { //extract name self.name = [self.bundle infoDictionary][@"CFBundleName"]; } //no app bundle || no 'CFBundleName' // just use last component from path if(nil == self.name) { //set name self.name = [self.path lastPathComponent]; } return; } //get file attributes -(void)getAttributes { //grab (file) attributes self.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self.path error:nil]; return; } //get (spotlight) meta data -(void)getMetadata { //md item ref MDItemRef mdItem = nil; //attributes names CFArrayRef attributeNames = nil; //create mdItem = MDItemCreate(kCFAllocatorDefault, (CFStringRef)self.path); if(nil == mdItem) { //bail goto bail; } //copy names attributeNames = MDItemCopyAttributeNames(mdItem); if(nil == attributeNames) { //bail goto bail; } //get metadata self.metadata = CFBridgingRelease(MDItemCopyAttributes(mdItem, attributeNames)); bail: //release names if(NULL != attributeNames) { //release CFRelease(attributeNames); attributeNames = NULL; } //release md item if(NULL != mdItem) { //release CFRelease(mdItem); mdItem = NULL; } return; } //get an icon for a process // for apps, this will be app's icon, otherwise just a standard system one -(void)getIcon { //icon's file name NSString* iconFile = nil; //icon's path NSString* iconPath = nil; //icon's path extension NSString* iconExtension = nil; //skip 'short' paths // otherwise system logs an error if( (YES != [self.path hasPrefix:@"/"]) && (nil == self.bundle) ) { //bail goto bail; } //for app's // extract their icon if(nil != self.bundle) { //get file iconFile = self.bundle.infoDictionary[@"CFBundleIconFile"]; //get path extension iconExtension = [iconFile pathExtension]; //if its blank (i.e. not specified) // go with 'icns' if(YES == [iconExtension isEqualTo:@""]) { //set type iconExtension = @"icns"; } //set full path iconPath = [self.bundle pathForResource:[iconFile stringByDeletingPathExtension] ofType:iconExtension]; //load it self.icon = [[NSImage alloc] initWithContentsOfFile:iconPath]; } //process is not an app or couldn't get icon // try to get it via shared workspace if( (nil == self.bundle) || (nil == self.icon) ) { //extract icon self.icon = [[NSWorkspace sharedWorkspace] iconForFile:self.path]; } //make standard size... [self.icon setSize:NSMakeSize(128, 128)]; bail: return; } //statically, generate signing info -(void)generateSigningInfo:(SecCSFlags)flags { //signing info NSMutableDictionary* extractedSigningInfo = nil; //extract signing info extractedSigningInfo = extractSigningInfo(0, self.path, flags); //valid? // save into iVar if( (nil != extractedSigningInfo[KEY_CS_STATUS]) && (noErr == [extractedSigningInfo[KEY_CS_STATUS] intValue])) { //save self.csInfo = extractedSigningInfo; } //dbg msg else os_log_debug(logHandle, "invalid code signing information for %{public}@: %{public}@", self.path, extractedSigningInfo); return; } //for pretty printing -(NSString *)description { //pretty print return [NSString stringWithFormat: @"name: %@\npath: %@\nattributes: %@\nsigning info: %@\nmetadata: %@", self.name, self.path, self.attributes, self.metadata, self.csInfo]; } @end ================================================ FILE: LuLu/Extension/BlockOrAllowList.h ================================================ // // BlockOrAllowList.h // Extension // // Created by Patrick Wardle on 11/6/20. // Copyright © 2020 Objective-See. All rights reserved. // @import Cocoa; @import OSLog; @import NetworkExtension; NS_ASSUME_NONNULL_BEGIN @interface BlockOrAllowList : NSObject /* PROPERTIES */ //path @property(nonatomic, retain)NSString* path; //block list @property(nonatomic, retain)NSMutableSet* items; //modification time @property(nonatomic, retain)NSDate* lastModified; /* METHODS */ //init // with a path -(id)init:(NSString*)path; //(re)load from disk -(void)load:(NSString*)path; //should reload // checks file modification time -(BOOL)shouldReload; //check if flow matches item on block list -(BOOL)isMatch:(NEFilterSocketFlow*)flow; @end NS_ASSUME_NONNULL_END ================================================ FILE: LuLu/Extension/BlockOrAllowList.m ================================================ // // BlockOrAllowList.m // Extension // // Created by Patrick Wardle on 11/6/20. // Copyright © 2020 Objective-See. All rights reserved. // #import "consts.h" #import "Preferences.h" #import "BlockOrAllowList.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //preferences extern Preferences* preferences; @implementation BlockOrAllowList -(id)init:(NSString*)path { //init super self = [super init]; if(nil != self) { //save list self.path = path; //load [self load:self.path]; } return self; } //was specified block list remote // ...just checks if prefixed with http:// || https:// -(BOOL)isRemote { //specified path a URL? return ((YES == [self.path hasPrefix:@"http://"]) || (YES == [self.path hasPrefix:@"https://"])); } //should reload // checks file modification time -(BOOL)shouldReload { //flag BOOL shouldReload = NO; //current mod. time NSDate* modified = nil; //if it's remote // can't tell, so default to no if(YES == [self isRemote]) { //bail goto bail; } //get modified timestamp modified = [[NSFileManager.defaultManager attributesOfItemAtPath:self.path error:nil] objectForKey:NSFileModificationDate]; //was file modified? if(NSOrderedDescending == [modified compare:self.lastModified]) { //dbg msg os_log_debug(logHandle, "block list was modified ...will reload"); //yes shouldReload = YES; } bail: return shouldReload; } //(re)load -(void)load:(NSString*)path { //error NSError* error = nil; //file contents NSString* list = nil; //sync @synchronized (self) { //update path self.path = path; //reset list [self.items removeAllObjects]; //dbg msg os_log_debug(logHandle, "%s", __PRETTY_FUNCTION__); //check // path? if(0 == self.path.length) { //dbg msg os_log_debug(logHandle, "no list specified..."); //bail goto bail; } //remote? // load via URL if(YES == [self isRemote]) { //dbg msg os_log_debug(logHandle, "(re)loading (remote) list"); //load list = [NSString stringWithContentsOfURL:[NSURL URLWithString:self.path] encoding:NSUTF8StringEncoding error:&error]; if(nil != error) { //err msg os_log_error(logHandle, "ERROR: failed to (re)load (remote) list, %{public}@ (error: %{public}@)", self.path, error); //bail goto bail; } //(re)load remote URL once a day dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(24 * 60 * 60 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ //dbg msg os_log_debug(logHandle, "(re)loading (remote) list"); //(re)load [self load:self.path]; }); } //local file // check and load else { //dbg msg os_log_debug(logHandle, "(re)loading (local) list, %{public}@", self.path); //(re)load list = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:&error]; if(nil != error) { //err msg os_log_error(logHandle, "ERROR: failed to (re)load (local) list, %{public}@ (error: %{public}@)", self.path, error); //bail goto bail; } //save timestamp self.lastModified = [[NSFileManager.defaultManager attributesOfItemAtPath:self.path error:nil] objectForKey:NSFileModificationDate]; } //init set // of trimmed/lower-cased items self.items = [NSMutableSet setWithArray:[[[list componentsSeparatedByString:@"\n"] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *item, NSDictionary *bindings) { //trim NSString* trimmed = [item stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; //make sure its not empty/not a comment return (trimmed.length > 0 && ![trimmed hasPrefix:@"#"]); }]] valueForKey:@"lowercaseString"]]; //dbg msg os_log_debug(logHandle, "(re)loaded %lu list items", (unsigned long)self.items.count); } //sync bail: return; } //check if flow matches item on block or allow list // note: currently lists don't support port matching -(BOOL)isMatch:(NEFilterSocketFlow*)flow { //match BOOL isMatch = NO; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //endpoint url/hosts NSMutableSet* endpointNames = nil; //matches NSSet* matches = nil; //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)flow.remoteEndpoint; //need to reload list? // checks timestamp to see if modified if(YES == [self shouldReload]) { //(re)load list [self load:self.path]; } //sync @synchronized (self) { //init endpoint names endpointNames = [NSMutableSet set]; //add url if(nil != flow.URL.absoluteString) { //add full url [endpointNames addObject:flow.URL.absoluteString.lowercaseString]; } //add host if(nil != flow.URL.host) { //add full url [endpointNames addObject:flow.URL.host.lowercaseString]; } //add host name if(nil != remoteEndpoint.hostname) { //add [endpointNames addObject:remoteEndpoint.hostname.lowercaseString]; } //macOS 11+? // add remote host name if(@available(macOS 11, *)) { //add remote host name if(nil != flow.remoteHostname) { //add [endpointNames addObject:flow.remoteHostname.lowercaseString]; //if it starts w/ 'www.' // strip and add that too if(YES == [flow.remoteHostname hasPrefix:@"www."]) { //add [endpointNames addObject:[[flow.remoteHostname substringFromIndex:4] lowercaseString]]; } } } //first check for "all" // for IPV4 -> '0.0.0.0/0' if( (AF_INET == flow.socketFamily) && ([self.items containsObject:@"0.0.0.0/0"]) ) { isMatch = YES; goto bail; } //for IPV6 -> '::/0' else if( (AF_INET6 == flow.socketFamily) && ([self.items containsObject:@"::/0"]) ) { isMatch = YES; goto bail; } //find matches matches = [self.items objectsPassingTest:^BOOL(NSString* item, BOOL* stop) { return [endpointNames containsObject:item]; }]; //any matches? if(0 != matches.count) { //dbg msg os_log_debug(logHandle, "endpoint names %{public}@ matched the following list items %{public}@", endpointNames, matches); //set flag isMatch = YES; } }//sync bail: return isMatch; } @end ================================================ FILE: LuLu/Extension/Extension.entitlements ================================================ com.apple.developer.networking.networkextension content-filter-provider-systemextension com.apple.security.application-groups $(TeamIdentifierPrefix)com.objective-see.lulu com.apple.security.temporary-exception.files.absolute-path.read-write /private/var/db/mds/ ================================================ FILE: LuLu/Extension/FilterDataProvider.h ================================================ // // FilterDataProvider.h // LuLu // // Created by Patrick Wardle on 8/1/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #import @import OSLog; @import NetworkExtension; #import "GrayList.h" @interface FilterDataProvider : NEFilterDataProvider /* PROPERTIES */ //(process) cache @property(atomic, retain)NSCache* cache; //graylist obj @property(nonatomic, retain)GrayList* grayList; //related flows @property(nonatomic, retain)NSMutableDictionary* relatedFlows; /* METHODS */ //get best hostname from flow // prioritizes domain names over IP addresses -(NSString*)getBestHostnameFromFlow:(NEFilterSocketFlow*)flow; @end ================================================ FILE: LuLu/Extension/FilterDataProvider.m ================================================ // // FilterDataProvider.m // LuLu // // Created by Patrick Wardle on 8/1/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #import "Rule.h" #import "Rules.h" #import "Alerts.h" #import "consts.h" #import "GrayList.h" #import "BlockOrAllowList.h" #import "utilities.h" #import "Preferences.h" #import "XPCUserProto.h" #import "FilterDataProvider.h" //verdicts typedef NS_ENUM(NSInteger, FlowVerdict) { kFlowVerdictAllow, kFlowVerdictBlock, kFlowVerdictPause, // new alert shown, waiting for user kFlowVerdictRelated, // another alert already shown for this process }; /* GLOBALS */ //alerts extern Alerts* alerts; //log handle extern os_log_t logHandle; //rules extern Rules* rules; //preferences extern Preferences* preferences; //allow list extern BlockOrAllowList* allowList; //block list extern BlockOrAllowList* blockList; @implementation FilterDataProvider @synthesize cache; @synthesize grayList; //init -(id)init { //super self = [super init]; if(nil != self) { //init cache cache = [[NSCache alloc] init]; //set cache limit self.cache.countLimit = 2048; //init gray list grayList = [[GrayList alloc] init]; //alloc related flows self.relatedFlows = [NSMutableDictionary dictionary]; } return self; } //start filter -(void)startFilterWithCompletionHandler:(void (^)(NSError *error))completionHandler { //rules NSMutableArray* rules = nil; //network rules NENetworkRule* anyOutboundRule = nil; NENetworkRule* loopbackRule4 = nil; NENetworkRule* loopbackRule6 = nil; //filter settings NEFilterSettings* filterSettings = nil; //log msg os_log_debug(logHandle, "%s", __PRETTY_FUNCTION__); //init rules array rules = [NSMutableArray array]; //Rule 1: // IPv4 loopback (127.0.0.0/8), any port NWHostEndpoint* loopback4 = [NWHostEndpoint endpointWithHostname:@"127.0.0.0" port:@"0"]; loopbackRule4 = [[NENetworkRule alloc] initWithRemoteNetwork:loopback4 remotePrefix:8 localNetwork:nil localPrefix:0 protocol:NENetworkRuleProtocolAny direction:NETrafficDirectionOutbound]; [rules addObject:[[NEFilterRule alloc] initWithNetworkRule:loopbackRule4 action:NEFilterActionFilterData]]; //Rule 2: // IPv6 loopback (::1/128), any port NWHostEndpoint* loopback6 = [NWHostEndpoint endpointWithHostname:@"::1" port:@"0"]; loopbackRule6 = [[NENetworkRule alloc] initWithRemoteNetwork:loopback6 remotePrefix:128 localNetwork:nil localPrefix:0 protocol:NENetworkRuleProtocolAny direction:NETrafficDirectionOutbound]; [rules addObject:[[NEFilterRule alloc] initWithNetworkRule:loopbackRule6 action:NEFilterActionFilterData]]; //Rule 3: // any/all outbound traffic (non-loopback) anyOutboundRule = [[NENetworkRule alloc] initWithRemoteNetwork:nil remotePrefix:0 localNetwork:nil localPrefix:0 protocol:NENetworkRuleProtocolAny direction:NETrafficDirectionOutbound]; [rules addObject:[[NEFilterRule alloc] initWithNetworkRule:anyOutboundRule action:NEFilterActionFilterData]]; //init filter settings filterSettings = [[NEFilterSettings alloc] initWithRules:rules defaultAction:NEFilterActionAllow]; //apply rules [self applySettings:filterSettings completionHandler:^(NSError * _Nullable error) { //log msg os_log_debug(logHandle, "'applySettings' completed"); //error? if(nil != error) os_log_error(logHandle, "ERROR: failed to apply filter settings: %@", error.localizedDescription); //call completion handler completionHandler(error); }]; return; } //stop filter -(void)stopFilterWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler { //log msg os_log_debug(logHandle, "method '%s' invoked with %ld", __PRETTY_FUNCTION__, (long)reason); //extra dbg info if(NEProviderStopReasonUserInitiated == reason) { //log msg os_log_debug(logHandle, "reason: NEProviderStopReasonUserInitiated"); } //required completionHandler(); return; } //handle flow -(NEFilterNewFlowVerdict *)handleNewFlow:(NEFilterFlow *)flow { //socket flow NEFilterSocketFlow* socketFlow = nil; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //verdict NEFilterNewFlowVerdict* verdict = nil; //log msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //init verdict to allow verdict = [NEFilterNewFlowVerdict allowVerdict]; //no prefs (yet) or disabled // just allow the flow (don't block) if( (0 == preferences.preferences.count) || (YES == [preferences.preferences[PREF_IS_DISABLED] boolValue]) ) { //dbg msg os_log_debug(logHandle, "no prefs (yet) || disabled, so allowing flow"); //bail goto bail; } //typecast socketFlow = (NEFilterSocketFlow*)flow; //log msg //os_log_debug(logHandle, "flow: %{public}@", flow); //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)socketFlow.remoteEndpoint; //log msg os_log_debug(logHandle, "remote endpoint: %{public}@ / url: %{public}@", remoteEndpoint, flow.URL); //ignore non-outbound traffic // even though we init'd `NETrafficDirectionOutbound`, sometimes get inbound traffic :| if(NETrafficDirectionOutbound != socketFlow.direction) { //log msg os_log_debug(logHandle, "ignoring non-outbound traffic (direction: %ld)", (long)socketFlow.direction); //bail goto bail; } //process flow // determine verdict/deliver alert switch([self processEvent:flow]) { //allow case kFlowVerdictAllow: os_log_debug(logHandle, "verdict: allow"); verdict = [NEFilterNewFlowVerdict allowVerdict]; break; //block case kFlowVerdictBlock: os_log_debug(logHandle, "verdict: block"); verdict = [NEFilterNewFlowVerdict dropVerdict]; break; //pause case kFlowVerdictPause: os_log_debug(logHandle, "verdict: pause"); verdict = [NEFilterNewFlowVerdict pauseVerdict]; break; //related // pause & save case kFlowVerdictRelated: { os_log_debug(logHandle, "verdict: related"); verdict = [NEFilterNewFlowVerdict pauseVerdict]; //save as related flow Process* process = [self.cache objectForKey:flow.sourceAppAuditToken]; if(process) { [self addRelatedFlow:process.key flow:socketFlow]; } //no process // just allow else { verdict = [NEFilterNewFlowVerdict allowVerdict]; } break; } } //log msg os_log_debug(logHandle, "verdict: %{public}@", verdict); bail: return verdict; } //process a network out event from the network extension (OS) // if there is no matching rule, will tell client to show alert -(FlowVerdict)processEvent:(NEFilterFlow*)flow { //process obj Process* process = nil; //flag BOOL csChange = NO; //matching rule obj Rule* matchingRule = nil; //console user NSString* consoleUser = nil; //rule info NSMutableDictionary* info = nil; //default to allow (on errors, etc) FlowVerdict verdict = kFlowVerdictAllow; //(ext) install date static NSDate* installDate = nil; //token static dispatch_once_t onceToken = 0; //grab console user consoleUser = getConsoleUser(); //check cache for process process = [self.cache objectForKey:flow.sourceAppAuditToken]; if(!process) { os_log_debug(logHandle, "no process found in cache, will create"); //create // also adds to cache process = [self createProcess:flow]; } //in cache else { //dbg msg os_log_debug(logHandle, "found process object in cache: %{public}@ (pid: %d)", process.path, process.pid); } //sanity check // process exited? deny pid_t pid = audit_token_to_pid(*(audit_token_t*)flow.sourceAppAuditToken.bytes); if(!isAlive(pid)) { //dbg msg os_log_debug(logHandle, "process %d has exited, DENYING flow", pid); //block verdict = kFlowVerdictBlock; goto bail; } //sanity check // no process? just allow... if(nil == process) { //err msg os_log_error(logHandle, "ERROR: failed to create process for flow, will allow"); //bail goto bail; } //CHECK: // different logged in user? // just allow flow, as we don't want to block their traffic if( (nil != consoleUser) && (nil != alerts.consoleUser) && (YES != [alerts.consoleUser isEqualToString:consoleUser]) ) { //dbg msg os_log_debug(logHandle, "current console user '%{public}@', is different than '%{public}@', so allowing flow: %{public}@", consoleUser, alerts.consoleUser, ((NEFilterSocketFlow*)flow).remoteEndpoint); //all set goto bail; } //CHECK: // client in (full) block mode? ...block! // unless there is an allow list set, which we'll check if(YES == [preferences.preferences[PREF_BLOCK_MODE] boolValue]) { //but allow list set? if( (YES == [preferences.preferences[PREF_USE_ALLOW_LIST] boolValue]) && (YES == [allowList isMatch:(NEFilterSocketFlow*)flow]) ) { //dbg msg os_log_debug(logHandle, "client in block mode, but flow matches item in allow list, so allowing"); //allow verdict = kFlowVerdictAllow; //all set goto bail; } //dbg msg os_log_debug(logHandle, "client in block mode (and item not on allow list), so disallowing %d/%{public}@", process.pid, process.binary.name); //deny verdict = kFlowVerdictBlock; //all set goto bail; } //CHECK: // client using (global) block list if( (YES == [preferences.preferences[PREF_USE_BLOCK_LIST] boolValue]) && (0 != [preferences.preferences[PREF_BLOCK_LIST] length]) ) { //dbg msg os_log_debug(logHandle, "client is using block list '%{public}@' (%lu items) ...will check for match", preferences.preferences[PREF_BLOCK_LIST], (unsigned long)blockList.items.count); //match in block list? if(YES == [blockList isMatch:(NEFilterSocketFlow*)flow]) { //dbg msg os_log_debug(logHandle, "flow matches item in block list, so denying"); //deny verdict = kFlowVerdictBlock; //all set goto bail; } //dbg msg else os_log_debug(logHandle, "remote endpoint/URL not on block list..."); } //CHECK: // client using (global) allow list if( (YES == [preferences.preferences[PREF_USE_ALLOW_LIST] boolValue]) && (0 != [preferences.preferences[PREF_ALLOW_LIST] length]) ) { //dbg msg os_log_debug(logHandle, "client is using allow list '%{public}@' (%lu items) ...will check for match", preferences.preferences[PREF_ALLOW_LIST], (unsigned long)allowList.items.count); //match in allow list? if(YES == [allowList isMatch:(NEFilterSocketFlow*)flow]) { //dbg msg os_log_debug(logHandle, "flow matches item in allow list, so allowing"); //allow verdict = kFlowVerdictAllow; //all set goto bail; } //dbg msg else os_log_debug(logHandle, "remote endpoint/URL not on allow list..."); } //CHECK: // allow localhost enabled? if([preferences.preferences[PREF_ALLOW_LOCALHOST] boolValue]) { NEFilterSocketFlow* socketFlow = (NEFilterSocketFlow*)flow; NWHostEndpoint* remoteEndpoint = (NWHostEndpoint*)socketFlow.remoteEndpoint; //localhost? if([self isLocalhostHostname:remoteEndpoint.hostname]) { os_log_debug(logHandle, "localhost allowed (preferences), so allowing loopback to %{public}@", remoteEndpoint); //allow verdict = kFlowVerdictAllow; //all set goto bail; } } //CHECK: // check for existing rule //existing rule for process? matchingRule = [rules find:process flow:(NEFilterSocketFlow*)flow csChange:&csChange]; if(nil != matchingRule) { //dbg msg os_log_debug(logHandle, "found matching rule for %d/%{public}@: %{public}@", process.pid, process.binary.name, matchingRule); //matching rule !global/!directory? // add its 'external' path (as might be different than original) if( (YES != matchingRule.isGlobal.boolValue) && (YES != matchingRule.isDirectory.boolValue) ) { //add path if(nil != process.path) { //add [rules.rules[process.key][KEY_PATHS] addObject:process.path]; } } //deny? // otherwise will default to allow if(RULE_STATE_BLOCK == matchingRule.action.intValue) { //dbg msg os_log_debug(logHandle, "setting verdict to: BLOCK"); //deny verdict = kFlowVerdictBlock; } //allow (msg) else os_log_debug(logHandle, "rule says: ALLOW"); //all set goto bail; } /* NO MATCHING RULE FOUND */ //cs change? // update item's rules with new code signing info // note: user will be informed about this, if/when alert is delivered if(YES == csChange) { //dbg msg os_log_debug(logHandle, "found rule set for %d/%{public}@: %{public}@, but code signing info has changed", process.pid, process.binary.name, matchingRule); //update cs info [rules updateCSInfo:process]; } //no matching rule found? else { //dbg msg os_log_debug(logHandle, "no (saved) rule found for %d/%{public}@", process.pid, process.binary.name); } //CHECK: // client in passive mode? // take action based on user's settting ...allow/block if(YES == [preferences.preferences[PREF_PASSIVE_MODE] boolValue]) { //dbg msg os_log_debug(logHandle, "client in passive mode..."); //user action: allow? if(PREF_PASSIVE_MODE_ALLOW == [preferences.preferences[PREF_PASSIVE_MODE_ACTION] integerValue]) { //dbg msg os_log_debug(logHandle, "passive mode: action is 'allow', so allowing %d/%{public}@", process.pid, process.binary.name); //allow verdict = kFlowVerdictAllow; } //user action: block? else { //dbg msg os_log_debug(logHandle, "passive mode: action is 'block', so blocking %d/%{public}@", process.pid, process.binary.name); //block verdict = kFlowVerdictBlock; } //create rule? if(PREF_PASSIVE_MODE_RULES_YES == [preferences.preferences[PREF_PASSIVE_MODE_RULES] integerValue]) { //dbg msg os_log_debug(logHandle, "passive mode: create rules is set, so creating rule for new connection"); //extract remote endpoint information NWHostEndpoint* remoteEndpoint = (NWHostEndpoint*)((NEFilterSocketFlow*)flow).remoteEndpoint; //init info for rule creation with specific endpoint information info = [@{KEY_PATH:process.path, KEY_TYPE:@RULE_TYPE_PASSIVE} mutableCopy]; //get best hostname (prioritizes domain names over IP addresses) NSString* bestHostname = [self getBestHostnameFromFlow:(NEFilterSocketFlow*)flow]; //add endpoint address (hostname) if available if(0!= bestHostname.length) { info[KEY_ENDPOINT_ADDR] = bestHostname; } else { info[KEY_ENDPOINT_ADDR] = VALUE_ANY; } //add endpoint port if available if(0 != remoteEndpoint.port.length) { info[KEY_ENDPOINT_PORT] = remoteEndpoint.port; } else { info[KEY_ENDPOINT_PORT] = VALUE_ANY; } //add protocol if available if(((NEFilterSocketFlow*)flow).socketProtocol > 0) { info[KEY_PROTOCOL] = [NSNumber numberWithInt:((NEFilterSocketFlow*)flow).socketProtocol]; } //add process cs info? if(nil != process.csInfo) info[KEY_CS_INFO] = process.csInfo; //add action: allow if(PREF_PASSIVE_MODE_ALLOW == [preferences.preferences[PREF_PASSIVE_MODE_ACTION] integerValue]) { //dbg msg os_log_debug(logHandle, "passive mode: creating rule with 'allow'"); //allow info[KEY_ACTION] = @RULE_STATE_ALLOW; } //add action: block else { //dbg msg os_log_debug(logHandle, "passive mode: creating rule with 'block'"); //block info[KEY_ACTION] = @RULE_STATE_BLOCK; } //create and add rule if(YES != [rules add:[[Rule alloc] init:info] save:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to add (passive) rule for %{public}@", info[KEY_PATH]); //bail goto bail; } //tell user rules changed [alerts.xpcUserClient rulesChanged]; } //no rule creation needed else { //dbg msg os_log_debug(logHandle, "passive mode: create rules is not set..."); } //all set goto bail; } //dbg msg os_log_debug(logHandle, "client not in passive mode..."); //CHECK: // there is related alert shown (i.e. for same process) // save this flow, as only want to process once user responds to first alert if(YES == [alerts isRelated:process]) { //dbg msg os_log_debug(logHandle, "an alert is shown for process %d/%{public}@, so holding off delivering for now...", process.pid, process.binary.name); //related // will pause verdict = kFlowVerdictRelated; //bail goto bail; } //dbg msg os_log_debug(logHandle, "no related alert, currently shown..."); //CHECK: // Apple process and 'PREF_ALLOW_APPLE' is set? Allow // Unless: // a) Its on the 'graylist' (e.g. curl) as these can be (ab)used by malware // b) There are other rules for this same process (even though they didn't match) if(YES == [preferences.preferences[PREF_ALLOW_APPLE] boolValue]) { //dbg msg os_log_debug(logHandle, "'Allow Apple' preference is set, will check if is an Apple binary"); //signed by Apple? if(Apple == [process.csInfo[KEY_CS_SIGNER] intValue]) { //dbg msg os_log_debug(logHandle, "is an Apple binary..."); //graylisted item? // pause and alert user if(YES == [self.grayList isGrayListed:process]) { //dbg msg os_log_debug(logHandle, "while signed by apple, %d/%{public}@ is gray listed, so will alert", process.pid, process.binary.name); //pause verdict = kFlowVerdictPause; //create/deliver alert [self alert:(NEFilterSocketFlow*)flow process:process csChange:csChange]; } //other rules for this process? else if(0 != [rules.rules[process.key][KEY_RULES] count]) { //dbg msg os_log_debug(logHandle, "while signed by apple, %d/%{public}@ has other (non-matching) rules, so will alert", process.pid, process.binary.name); //pause verdict = kFlowVerdictPause; //create/deliver alert [self alert:(NEFilterSocketFlow*)flow process:process csChange:csChange]; } //otherwise its a apple binary // not on graylist and w/ no other rules, so allow else { //dbg msg os_log_debug(logHandle, "due to preferences, allowing (non-graylisted) apple process %d/%{public}@", process.pid, process.path); //init for (rule) info // type: apple, action: allow info = [@{KEY_PATH:process.path, KEY_ACTION:@RULE_STATE_ALLOW, KEY_TYPE:@RULE_TYPE_APPLE} mutableCopy]; //add process cs info if(nil != process.csInfo) { //add info[KEY_CS_INFO] = process.csInfo; } //add key info[KEY_KEY] = process.key; //add/save if(YES != [rules add:[[Rule alloc] init:info] save:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to add rule"); //bail goto bail; } //tell user rules changed [alerts.xpcUserClient rulesChanged]; } //all set goto bail; } //signed by apple } //dbg msg else { //dbg msg os_log_debug(logHandle, "'Allow Apple' preference not set, so skipped 'Is Apple' check"); } //'allow installed' check // if preference is enabled, item is 3rd-party, internal, and hasn't had its CS changed ...allow! if( (YES == [preferences.preferences[PREF_ALLOW_INSTALLED] boolValue]) && (Apple != [process.csInfo[KEY_CS_SIGNER] intValue]) && (YES != csChange) ) { //only check internal processes // so, like ignore ones from DMGs, external drives, etc. if(YES == isInternalProcess(process.path)) { //app date NSDate* date = nil; //dbg msg os_log_debug(logHandle, "3rd-party (internal) app, plus 'PREF_ALLOW_INSTALLED' is set..."); //only once // get install date dispatch_once(&onceToken, ^{ //get LuLu's install date installDate = preferences.preferences[PREF_INSTALL_TIMESTAMP]; //dbg msg os_log_debug(logHandle, "LuLu's install date: %{public}@", installDate); }); //get item's date added date = dateAdded(process.path); if( (nil != date) && (NSOrderedAscending == [date compare:installDate]) ) { //dbg msg os_log_debug(logHandle, "3rd-party item was installed prior (%@) to LuLu (%@), allowing & adding rule", date, installDate); //init info for rule creation info = [@{KEY_PATH:process.path, KEY_ACTION:@RULE_STATE_ALLOW, KEY_TYPE:@RULE_TYPE_BASELINE} mutableCopy]; //add process cs info if(nil != process.csInfo) { info[KEY_CS_INFO] = process.csInfo; } //create and add rule if(YES != [rules add:[[Rule alloc] init:info] save:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to add rule for %{public}@", info[KEY_PATH]); //bail goto bail; } //tell user rules changed [alerts.xpcUserClient rulesChanged]; //all set goto bail; } //newer else { //dbg msg os_log_debug(logHandle, "3rd-party item date (%@), is after LuLu's install date (%@)", date, installDate); } } //item is external else { os_log_debug(logHandle, "%{public}@ is external, so skipping 'allow installed' check", process.path); } } //allow dns traffic pref set? // really, just any UDP traffic over port 53 if(YES == [preferences.preferences[PREF_ALLOW_DNS] boolValue]) { //dbg msg os_log_debug(logHandle, "'allow DNS traffic' is enabled, so checking port/protocol"); //check proto (UDP) and port (53) if( (IPPROTO_UDP == ((NEFilterSocketFlow*)flow).socketProtocol) && (YES == [((NWHostEndpoint*)((NEFilterSocketFlow*)flow).remoteEndpoint).port isEqualToString:@"53"]) ) { //dbg msg os_log_debug(logHandle, "protocol is 'UDP' and port is '53', (so likely DNS traffic) ...will allow" ); //allow verdict = kFlowVerdictAllow; //done goto bail; } } //allow simulator apps? if(YES == [preferences.preferences[PREF_ALLOW_SIMULATOR] boolValue]) { //dbg msg os_log_debug(logHandle, "'allow simulator apps' is enabled, so checking process"); //is simulator app? if(YES == isSimulatorApp(process.path)) { //dbg msg os_log_debug(logHandle, "%{public}@, is an simulator app, so will allow", process.path); //allow verdict = kFlowVerdictAllow; //done goto bail; } } //no user? // allow, but create rule for user to review if( (nil == consoleUser) || (nil == alerts.xpcUserClient) ) { //dbg msg os_log_debug(logHandle, "no active user or no connected client, will allow (and create rule)..."); //init info for rule creation info = [@{KEY_PATH:process.path, KEY_ACTION:@RULE_STATE_ALLOW, KEY_TYPE:@RULE_TYPE_PASSIVE} mutableCopy]; //add process cs info? if(nil != process.csInfo) info[KEY_CS_INFO] = process.csInfo; //create and add rule if(YES != [rules add:[[Rule alloc] init:info] save:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to add rule for %{public}@", info[KEY_PATH]); //bail goto bail; } //tell user rules changed [alerts.xpcUserClient rulesChanged]; //all set goto bail; } //sending to user, so pause! verdict = kFlowVerdictPause; //create/deliver alert // note: handles response + next/any related flow [self alert:(NEFilterSocketFlow*)flow process:process csChange:csChange]; bail: //log msg // match on this if you want detailed insight into LuLu's decision // log stream --level debug --predicate 'subsystem == "com.objective-see.lulu" && composedMessage BEGINSWITH "[LULU]"' os_log_debug(logHandle, "[LULU] PROCESS: %{public}@, FLOW (endpoint): %{public}@, RULE: %{public}@, verdict: %ld", process.path, ((NEFilterSocketFlow*)flow).remoteEndpoint, matchingRule, verdict); return verdict; } //1. Create and deliver alert //2. Handle response (and process other shown alerts, etc.) -(void)alert:(NEFilterSocketFlow*)flow process:(Process*)process csChange:(BOOL)csChange { //alert NSMutableDictionary* alert = nil; //rule __block Rule* rule = nil; //create alert alert = [alerts create:(NEFilterSocketFlow*)flow process:process]; //add cs change alert[KEY_CS_CHANGE] = [NSNumber numberWithBool:csChange]; //dbg msg os_log_debug(logHandle, "created alert..."); //deliver alert // and process user response if(YES != [alerts deliver:alert reply:^(NSDictionary* alert) { //verdict NEFilterNewFlowVerdict* verdict = nil; //log msg // note, this msg persists in log os_log(logHandle, "(user) response: \"%@\" for %{public}@, that was trying to connect to %{public}@:%{public}@", (RULE_STATE_BLOCK == [alert[KEY_ACTION] unsignedIntValue]) ? @"block" : @"allow", alert[KEY_PATH], alert[KEY_ENDPOINT_ADDR], alert[KEY_ENDPOINT_PORT]); //init verdict to allow verdict = [NEFilterNewFlowVerdict allowVerdict]; //user replied with block? if( (nil != alert[KEY_ACTION]) && (RULE_STATE_BLOCK == [alert[KEY_ACTION] unsignedIntValue]) ) { //verdict: block verdict = [NEFilterNewFlowVerdict dropVerdict]; } //resume flow w/ verdict [self resumeFlow:flow withVerdict:verdict]; //init rule rule = [[Rule alloc] init:alert]; //add / save [rules add:rule save:![rule isTemporary]]; //remove from 'shown' [alerts removeShown:alert]; //tell user rules changed [alerts.xpcUserClient rulesChanged]; //process (any) related flows // now that rule is created, related flows should match it or generate new alerts [self processRelatedFlow:alert[KEY_KEY]]; }]) { //failed to deliver, so allow [self resumeFlow:flow withVerdict:[NEFilterNewFlowVerdict allowVerdict]]; //process related flows [self processRelatedFlow:alert[KEY_KEY]]; } //delivered to user else { //save as shown // needed so related (same process!) alerts aren't delivered as well [alerts addShown:alert]; } return; } //add an alert to 'related' // invoked when there is already an alert shown for process // once user responds to alert, these will then be processed -(void)addRelatedFlow:(NSString*)key flow:(NEFilterSocketFlow*)flow { //dbg msg os_log_debug(logHandle, "adding flow to 'related': %{public}@ / %{public}@", key, flow); if(!key) { return; } //sync/save @synchronized(self.relatedFlows) { //first time // init array for item (process) alerts if(!self.relatedFlows[key]) { self.relatedFlows[key] = [NSMutableArray array]; } //only add if new if(![self.relatedFlows[key] containsObject:flow]) { [self.relatedFlows[key] addObject:flow]; } } return; } //process any related flows -(void)processRelatedFlow:(NSString*)key { //dbg msg os_log_debug(logHandle, "processing %lu related flow(s) for %{public}@", (unsigned long)[self.relatedFlows[key] count], key); while(YES) { NEFilterSocketFlow* flow = nil; //dequeue one flow @synchronized(self.relatedFlows) { NSMutableArray* queue = self.relatedFlows[key]; //done? if(!queue.count) { os_log_debug(logHandle, "drained (processed) all related flows"); [self.relatedFlows removeObjectForKey:key]; break; } flow = queue.firstObject; [queue removeObjectAtIndex:0]; } //process FlowVerdict flowVerdict = [self processEvent:flow]; //(still) related? // (re)add and be done for now if(flowVerdict == kFlowVerdictRelated) { os_log_debug(logHandle, "flow is (still) related"); [self addRelatedFlow:key flow:flow]; break; } //paused (asked user) // asked user, so be done for now too else if(flowVerdict == kFlowVerdictPause) { os_log_debug(logHandle, "flow is paused"); break; } //resume flow NEFilterNewFlowVerdict* verdict = (flowVerdict == kFlowVerdictBlock) ? [NEFilterNewFlowVerdict dropVerdict] : [NEFilterNewFlowVerdict allowVerdict]; os_log_debug(logHandle, "resuming related flow with %{public}@", verdict); [self resumeFlow:flow withVerdict:verdict]; } os_log_debug(logHandle, "done processing related flows"); } //create process object -(Process*)createProcess:(NEFilterFlow*)flow { //audit token audit_token_t* token = NULL; //process obj Process* process = nil; //extract (audit) token token = (audit_token_t*)flow.sourceAppAuditToken.bytes; //init process object, via audit token process = [[Process alloc] init:token]; if(nil == process) { //err msg os_log_error(logHandle, "ERROR: failed to create process for %d", audit_token_to_pid(*token)); //bail goto bail; } //sync to add to cache @synchronized(self.cache) { //add to cache [self.cache setObject:process forKey:flow.sourceAppAuditToken]; } bail: return process; } //get best hostname from flow // prioritizes domain names over IP addresses // uses same logic as active mode rule matching -(NSString*)getBestHostnameFromFlow:(NEFilterSocketFlow*)flow { //best hostname NSString* bestHostname = nil; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)flow.remoteEndpoint; //priority 1: try flow.URL.host (best for domain names) if(flow.URL.host.length) { //dbg msg os_log_debug(logHandle, "using flow.URL.host as best hostname: %{public}@", flow.URL.host); //use it bestHostname = flow.URL.host; //done goto bail; } //priority 2: try flow.remoteHostname (macOS 11+) if(@available(macOS 11, *)) { if(flow.remoteHostname.length) { //dbg msg os_log_debug(logHandle, "using flow.remoteHostname as best hostname: %{public}@", flow.remoteHostname); //use it bestHostname = flow.remoteHostname; //done goto bail; } } //priority 3: fallback to remoteEndpoint.hostname (may be IP address) if(remoteEndpoint.hostname.length) { //dbg msg os_log_debug(logHandle, "using remoteEndpoint.hostname as fallback hostname: %{public}@", remoteEndpoint.hostname); //use it bestHostname = remoteEndpoint.hostname; } bail: //dbg msg os_log_debug(logHandle, "best hostname for flow: %{public}@", bestHostname); return bestHostname; } //check if hostname is a valid localhost address -(BOOL)isLocalhostHostname:(NSString*)hostname { struct sockaddr_in sa4 = {0}; struct sockaddr_in6 sa6 = {0}; //sanity check if(!hostname.length) { return NO; } //exact matches for localhost or IPv6 loopback if([hostname isEqualToString:@"::1"] || [hostname isEqualToString:@"localhost"]) { return YES; } //check for valid IPv4 loopback range (127.0.0.0/8) if(inet_pton(AF_INET, hostname.UTF8String, &(sa4.sin_addr)) == 1) { return IN_LOOPBACK(ntohl(sa4.sin_addr.s_addr)); } //check for valid IPv6 loopback (::1) if(inet_pton(AF_INET6, [hostname UTF8String], &(sa6.sin6_addr)) == 1) { return IN6_IS_ADDR_LOOPBACK(&sa6.sin6_addr); } //not a valid localhost address return NO; } @end ================================================ FILE: LuLu/Extension/GrayList.h ================================================ // // file: GrayList.h // project: lulu (launch daemon) // description: gray listed binaries (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import OSLog; @import Foundation; @interface GrayList : NSObject /* PROPERTIES */ //gray listed (apple) binaries @property(nonatomic, retain)NSMutableSet* graylistedBinaries; /* METHODS */ //determine if process is graylisted -(BOOL)isGrayListed:(Process*)process; @end ================================================ FILE: LuLu/Extension/GrayList.m ================================================ // // file: GrayList.m // project: lulu (launch daemon) // description: gray listed binaries // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "Process.h" #import "GrayList.h" //apple system utils that aren't allowed by default // these may be abused by malware, so will make sure they trigger an alert NSString* const GRAYLISTED_BINARIES[] = { @"com.apple.nc", @"com.apple.ftp", @"com.apple.zsh", @"com.apple.ksh", @"com.apple.php", @"com.apple.scp", @"com.apple.ssh", @"com.apple.bash", @"com.apple.tcsh", @"com.apple.curl", @"com.apple.perl", @"com.apple.ruby", @"com.apple.sftp", @"com.tcltk.tclsh", @"com.apple.perl5", @"com.apple.whois", @"com.apple.python", @"com.apple.telnet", @"com.apple.openssh", @"com.apple.python2", @"com.apple.python3", @"org.python.python", @"com.apple.pythonw", @"com.apple.osascript", }; /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation GrayList @synthesize graylistedBinaries; //init -(id)init { //init super self = [super init]; if(nil != self) { //init list (set) of gray listed binaries graylistedBinaries = [NSMutableSet set]; //add each to set for(NSUInteger i=0; i CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName LuLu CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright (c) 2020 Objective-See. All rights reserved. NSSystemExtensionUsageDescription LuLu needs to load its System Extension to block unauthorized network connections. NetworkExtension NEMachServiceName $(TeamIdentifierPrefix)com.objective-see.lulu NEProviderClasses com.apple.networkextension.filter-data FilterDataProvider ================================================ FILE: LuLu/Extension/Preferences.h ================================================ // // Preferences.h // launchDaemon // // Created by Patrick Wardle on 2/22/18. // Copyright (c) 2018 Objective-See. All rights reserved. // @import OSLog; @import Foundation; @interface Preferences : NSObject /* PROPERTIES */ //preferences @property(nonatomic, retain)NSMutableDictionary* preferences; /* METHODS */ //load prefs from disk -(BOOL)load; //update prefs // saves and handles logic for specific prefs -(BOOL)update:(NSDictionary*)updates replace:(BOOL)replace; //save to disk -(BOOL)save; //get current profile // this is saved in the default preferences (and may be nil) -(NSString*)getCurrentProfile; //set current profile // this is saved in the default preferences (and may be nil) -(void)setCurrentProfile:(NSString*)profilePath; @end ================================================ FILE: LuLu/Extension/Preferences.m ================================================ // // Preferences.m // launchDaemon // // Created by Patrick Wardle on 2/22/18. // Copyright (c) 2018 Objective-See. All rights reserved. // #import "consts.h" #import "BlockOrAllowList.h" #import "Preferences.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //allow list extern BlockOrAllowList* allowList; //block list extern BlockOrAllowList* blockList; @implementation Preferences @synthesize preferences; //init // loads prefs -(id)init { //super self = [super init]; if(nil != self) { //default prefs exist? // load them from disk if(YES == [NSFileManager.defaultManager fileExistsAtPath:[INSTALL_DIRECTORY stringByAppendingPathComponent:PREFS_FILE]]) { //load if(YES != [self load]) { //err msg os_log_error(logHandle, "ERROR: failed to loads preferences from %@", PREFS_FILE); //unset self = nil; //bail goto bail; } } //no prefs (yet) // just initialze empty dictionary else { //init self.preferences = [NSMutableDictionary dictionary]; } } bail: return self; } //get path to preferences // either default, or in current profile directory -(NSString*)path { //current profile NSString* currentProfile = nil; //path // init with default NSString* path = [INSTALL_DIRECTORY stringByAppendingPathComponent:PREFS_FILE]; //not found? // that ok, likely just first time if(YES != [NSFileManager.defaultManager fileExistsAtPath:path]) { //dbg msg os_log_debug(logHandle, "default preferences file '%{public}@' not found ...first time?", path); //done goto bail; } //get current profile currentProfile = [self getCurrentProfile]; if(nil != currentProfile) { //set path = [currentProfile stringByAppendingPathComponent:PREFS_FILE]; } bail: //dbg msg os_log_debug(logHandle, "using preferences file: %{public}@", path); return path; } //load prefs from disk -(BOOL)load { //flag BOOL loaded = NO; //path NSString* prefsFile = [self path]; //load self.preferences = [NSMutableDictionary dictionaryWithContentsOfFile:prefsFile]; if(nil == self.preferences) { //err msg os_log_error(logHandle, "ERROR: failed to load preference from %{public}@", prefsFile); goto bail; } //dbg msg os_log_debug(logHandle, "from %{public}@, loaded preferences: %{public}@", prefsFile, self.preferences); //set any defaults [self setDefaults]; //happy loaded = YES; bail: return loaded; } //set any defaults // needed as upgrades don't (re)display welcome window -(void)setDefaults { //flag BOOL shouldSave = NO; //PREF_ALLOW_LOCALHOST: default to YES for existing users if(!self.preferences[PREF_ALLOW_LOCALHOST]) { os_log_debug(logHandle, "setting default value for PREF_ALLOW_LOCALHOST: YES"); //set default self.preferences[PREF_ALLOW_LOCALHOST] = @YES; shouldSave = YES; } //could add any others here... //save if(shouldSave) { [self save]; } } //update prefs // handles logic for specific prefs & then saves -(BOOL)update:(NSDictionary*)updates replace:(BOOL)replace { //flag BOOL updated = NO; //allow list NSString* allowListPath = nil; //block list NSString* blockListPath = nil; //sync @synchronized (self) { //replace? // e.g. new profile if(YES == replace) { //dbg msg os_log_debug(logHandle, "replacing preferences (%{public}@)", updates); //replace self.preferences = [updates mutableCopy]; } //merge else { //dbg msg os_log_debug(logHandle, "updating preferences (%{public}@)", updates); //add in (new) prefs [self.preferences addEntriesFromDictionary:updates]; } //save if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save preferences"); //bail goto bail; } //extract any allow list allowListPath = updates[PREF_ALLOW_LIST]; //extract any block list blockListPath = updates[PREF_BLOCK_LIST]; //(new) allow list? // now, trigger reload if(0 != [allowListPath length]) { //dbg msg os_log_debug(logHandle, "user specified new 'allow' list: %{public}@", allowListPath); //first time? if(nil == allowList) { //alloc/init/load block list allowList = [[BlockOrAllowList alloc] init:allowListPath]; } //otherwise just reload else { //(re)load [allowList load:allowListPath]; } } //(new) block list? // now, trigger reload if(0 != [blockListPath length]) { //dbg msg os_log_debug(logHandle, "user specified new 'block' list: %{public}@", blockListPath); //first time? if(nil == blockList) { //alloc/init/load block list blockList = [[BlockOrAllowList alloc] init:blockListPath]; } //otherwise just reload else { //(re)load [blockList load:blockListPath]; } } //happy updated = YES; } //sync bail: return updated; } //save to disk -(BOOL)save { //flag BOOL wasSaved = NO; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //init w/ default NSString* prefsFile = [self path]; //write out preferences if(YES != [self.preferences writeToFile:prefsFile atomically:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to save preferences to: %{public}@", prefsFile); goto bail; } //dbg msg os_log_debug(logHandle, "saved preferences to %{public}@", prefsFile); //happy wasSaved = YES; bail: return wasSaved; } //get current profile // this is saved in the default preferences (and may be nil) -(NSString*)getCurrentProfile { //current NSString* currentProfile = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //load *default* prefs NSDictionary* defaultPreferences = [NSDictionary dictionaryWithContentsOfFile:[INSTALL_DIRECTORY stringByAppendingPathComponent:PREFS_FILE]]; //extract currentProfile = defaultPreferences[PREF_CURRENT_PROFILE]; //dbg msg os_log_debug(logHandle, "returning current profile %{public}@", currentProfile); return currentProfile; } //set current profile // this is saved in the *default* preferences (and may be nil) -(void)setCurrentProfile:(NSString*)profilePath { //default pref's file NSString* defaultPreferencesFile = [INSTALL_DIRECTORY stringByAppendingPathComponent:PREFS_FILE]; //load *default* prefs NSMutableDictionary* defaultPreferences = [NSMutableDictionary dictionaryWithContentsOfFile:defaultPreferencesFile]; //set defaultPreferences[PREF_CURRENT_PROFILE] = profilePath; //save [defaultPreferences writeToFile:defaultPreferencesFile atomically:YES]; //dbg msg os_log_debug(logHandle, "set current profile to %{public}@", profilePath); return; } @end ================================================ FILE: LuLu/Extension/Process.h ================================================ // // Process.h // LuLu // // Created by Patrick Wardle on 8/27/20. // Copyright (c) 2020 Objective-See. All rights reserved. // #ifndef Process_h #define Process_h @import OSLog; #import "Binary.h" @interface Process : NSObject /* PROPERTIES */ //pid @property pid_t pid; //user id @property uid_t uid; //type // used by process mon @property u_int16_t type; //exit code @property u_int32_t exit; //(self) deleted binary @property BOOL deleted; //name @property(nonatomic, retain)NSString* _Nullable name; //path @property(nonatomic, retain)NSString* _Nullable path; //args @property(nonatomic, retain)NSMutableArray* _Nullable arguments; //ancestors @property(nonatomic, retain)NSMutableArray* _Nullable ancestors; //signing info @property(nonatomic, retain)NSMutableDictionary* _Nullable csInfo; //key @property(nonatomic, retain)NSString* _Nonnull key; //Binary object // has path, hash, etc @property(nonatomic, retain)Binary* _Nonnull binary; //timestamp @property(nonatomic, retain)NSDate* _Nonnull timestamp; /* METHODS */ //init with a audit token // method will then (try) fill out rest of object -(id _Nullable)init:(audit_token_t* _Nonnull)token; @end #endif /* Process_h */ ================================================ FILE: LuLu/Extension/Process.m ================================================ // // File: Process.m // Project: Lulu // // Created by: Patrick Wardle // Copyright: 2017 Objective-See // #import "signing.h" #import "Process.h" #import "Utilities.h" #import #import #import #import /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation Process @synthesize pid; @synthesize exit; @synthesize path; @synthesize csInfo; @synthesize ancestors; @synthesize arguments; @synthesize timestamp; //init -(id)init { //init super self = [super init]; if(nil != self) { //alloc array for args arguments = [NSMutableArray array]; //alloc array for parents ancestors = [NSMutableArray array]; //set start time timestamp = [NSDate date]; //init pid self.pid = -1; //init user self.uid = -1; //init exit self.exit = -1; } return self; } //init with a token / pid // method will then (try) fill out rest of object -(id)init:(audit_token_t*)token { //current token NSData* currentToken = nil; //init self/super self = [self init]; if(self) { //save pid self.pid = audit_token_to_pid(*token); if(0 == self.pid) { //err msg os_log_error(logHandle, "ERROR: 'audit_token_to_pid' returned NULL\n"); return nil; } //get path // also sets 'self.deleted' iVar [self getPath:token]; if(0 == self.path.length) { //err msg os_log_error(logHandle, "ERROR: failed to find path for process %d\n", self.pid); return nil; } //set name //name for normal procs if(YES != self.deleted) { //get/add self.name = getProcessName(0, self.path); } //for delete procs // get path via pid else { //get/add self.name = getProcessName(self.pid, self.path); } //get user self.uid = audit_token_to_euid(*token); //generate (dynamic) code information [self generateSigningInfo:token]; //generate key // based on cs info, or path self.key = [self generateKey]; //init binary self.binary = [[Binary alloc] init:self.path]; /* pid specific logic note: pids can wrap, so we check audit token is still same! */ //set args [self getArgs]; //enum ancestors self.ancestors = generateProcessHierarchy(self.pid); //grab current (audit) token currentToken = tokenForPid(self.pid); if(0 != currentToken.length) { //check! // if it's changed, means pid points to new process, so unset parent, args, etc as these may be invalid! if(audit_token_to_pidversion(*token) != audit_token_to_pidversion(*(audit_token_t*)currentToken.bytes)) { //err msg os_log_error(logHandle, "ERROR: audit token mismatch ...pid re-used?"); //unset arguments = nil; ancestors = nil; } } } return self; } //generate key // note: this matches rules' generate key algo -(NSString*)generateKey { //id NSString* key = nil; //signer NSInteger signer = None; //cs info? if(nil != self.csInfo) { //extract signer signer = [self.csInfo[KEY_CS_SIGNER] intValue]; //apple/app store // just use cs id if( (Apple == signer) || (AppStore == signer) ) { //set key key = self.csInfo[KEY_CS_ID]; } //dev id? // use cs id + (leaf) signer else if(DevID == signer) { //check for cs id/auths if( (0 != [self.csInfo[KEY_CS_ID] length]) && (0 != [self.csInfo[KEY_CS_AUTHS] count]) ) { //set key = [NSString stringWithFormat:@"%@:%@", self.csInfo[KEY_CS_ID], [self.csInfo[KEY_CS_AUTHS] firstObject]]; } } } //no valid cs info, etc // just use item's path if(0 == key.length) { //set key = self.path; } //dbg msg os_log_debug(logHandle, "generated process key: %{public}@", key); return key; } //set process's path -(void)getPath:(audit_token_t*)token { //status OSStatus status = !errSecSuccess; //code ref SecCodeRef code = NULL; //path CFURLRef path = nil; //obtain code ref status = SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(@{(__bridge NSString *)kSecGuestAttributeAudit:[NSData dataWithBytes:token length:sizeof(audit_token_t)]}), kSecCSDefaultFlags, &code); if(errSecSuccess == status) { //copy path status = SecCodeCopyPath(code, kSecCSDefaultFlags, &path); if(errSecSuccess == status) { //extract/copy path self.path = [((__bridge NSURL*)path).path copy]; } //err msg else { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCopyPath' failed with': %#x", status); } } //err msg else { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCopyGuestWithAttributes' failed with': %#x", status); } //process's binary deleted? if(kPOSIXErrorENOENT == status) { //dbg msg os_log_debug(logHandle, "process %d's binary appears to be deleted", pid); //set flag self.deleted = YES; } //path (still) nil? // try other methods if(nil == path) { //get path via pid self.path = getProcessPath(self.pid); } //resolve symlinks self.path = [self.path stringByResolvingSymlinksInPath]; //free path if(NULL != path) { //free CFRelease(path); path = NULL; } //free code ref if(NULL != code) { //free CFRelease(code); code = NULL; } return; } //extract commandline args // saves into 'arguments' ivar -(void)getArgs { //'management info base' array int mib[3] = {0}; //system's size for max args int systemMaxArgs = 0; //process's args char* processArgs = NULL; //# of args int numberOfArgs = 0; //arg NSString* argument = nil; //start of (each) arg char* argStart = NULL; //size of buffers, etc size_t size = 0; //parser pointer char* parser = NULL; //init mib // want system's size for max args mib[0] = CTL_KERN; mib[1] = KERN_ARGMAX; //set size size = sizeof(systemMaxArgs); //get system's size for max args if(-1 == sysctl(mib, 2, &systemMaxArgs, &size, NULL, 0)) { //bail goto bail; } //alloc space for args processArgs = malloc(systemMaxArgs); if(NULL == processArgs) { //bail goto bail; } //init mib // want process args mib[0] = CTL_KERN; mib[1] = KERN_PROCARGS2; mib[2] = pid; //set size size = (size_t)systemMaxArgs; //get process's args if(-1 == sysctl(mib, 3, processArgs, &size, NULL, 0)) { //bail goto bail; } //sanity check // ensure buffer is somewhat sane if(size <= sizeof(int)) { //bail goto bail; } //extract number of args // at start of buffer memcpy(&numberOfArgs, processArgs, sizeof(numberOfArgs)); //init pointer to start of args // they start right after # of args parser = processArgs + sizeof(numberOfArgs); //scan until end of process's NULL-terminated path while(parser < &processArgs[size]) { //scan till NULL-terminator if(0x0 == *parser) { //end of exe name break; } //next char parser++; } //sanity check // make sure end-of-buffer wasn't reached if(parser == &processArgs[size]) { //bail goto bail; } //skip all trailing NULLs // scan will end when non-NULL is found while(parser < &processArgs[size]) { //scan till NULL-terminator if(0x0 != *parser) { //ok, got to argv[0] break; } //next char parser++; } //sanity check // (again), make sure end-of-buffer wasn't reached if(parser == &processArgs[size]) { //bail goto bail; } //parser should now point to argv[0], process name // init arg start argStart = parser; //keep scanning until all args are found // each is NULL-terminated while(parser < &processArgs[size]) { //each arg is NULL-terminated // so scan till NULL, then save into array if(*parser == '\0') { //save arg if(NULL != argStart) { //try convert // ignore (if not UTF8, etc...) argument = [NSString stringWithUTF8String:argStart]; if(nil != argument) { //save [self.arguments addObject:argument]; } } //init string pointer to (possibly) next arg argStart = ++parser; //bail if we've hit arg cnt if(self.arguments.count == numberOfArgs) { //bail break; } } //next char parser++; } bail: //free process args if(NULL != processArgs) { //free free(processArgs); //unset processArgs = NULL; } return; } //generate signing info -(void)generateSigningInfo:(audit_token_t*)token { //signing info NSMutableDictionary* extractedSigningInfo = nil; //extract signing info extractedSigningInfo = extractSigningInfo(token, nil, kSecCSDefaultFlags); //valid? // save into iVar if( (nil != extractedSigningInfo[KEY_CS_STATUS]) && (noErr == [extractedSigningInfo[KEY_CS_STATUS] intValue])) { //save self.csInfo = extractedSigningInfo; } //invalid else { //error msg os_log_error(logHandle, "ERROR: invalid code signing information for %{public}@: %{public}@", self.path, extractedSigningInfo); } return; } //for pretty printing -(NSString *)description { //pretty print return [NSString stringWithFormat: @"pid: %d\npath: %@\nuser: %d\nargs: %@\nancestors: %@\n signing info: %@\n binary:\n%@", self.pid, self.path, self.uid, self.arguments, self.ancestors, self.csInfo, self.binary]; } @end ================================================ FILE: LuLu/Extension/Profiles.h ================================================ // // Profiles.h // // Created by Patrick Wardle on 06/21/25. // Copyright (c) 2025 Objective-See. All rights reserved. // @import OSLog; @import Foundation; @interface Profiles : NSObject /* PROPERTIES */ //profiles directory @property(nonatomic, retain)NSString* directory; /* METHODS */ -(NSMutableArray*)enumerate; -(void)set:(NSString*)profilePath; -(BOOL)add:(NSString*)name preferences:(NSDictionary*)preferences; -(BOOL)delete:(NSString*)name; @end ================================================ FILE: LuLu/Extension/Profiles.m ================================================ // // Profiles.m // // Created by Patrick Wardle on 06/21/25. // Copyright (c) 2025 Objective-See. All rights reserved. // #import "Rules.h" #import "consts.h" #import "Profiles.h" #import "Preferences.h" /* GLOBALS */ //log handle extern os_log_t logHandle; //rules extern Rules* rules; //preferences extern Preferences* preferences; @implementation Profiles //init // loads profiles -(id)init { //super self = [super init]; if(nil != self) { //set (base) directory self.directory = [INSTALL_DIRECTORY stringByAppendingPathComponent:PROFILE_DIRECTORY]; } return self; } //enumerate // return list of just profile *names* -(NSMutableArray*)enumerate { //list NSMutableArray* profiles = [NSMutableArray array]; //no profiles? // not problem, but just bail here if(![NSFileManager.defaultManager fileExistsAtPath:self.directory]) { //dbg msg os_log_debug(logHandle, "no profiles? didn't find profiles directory %{public}@", self.directory); goto bail; } //grab all items, saving only names of directories for(NSString* name in [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.directory error:nil]) { BOOL isDir = NO; NSString* fullPath = [self.directory stringByAppendingPathComponent:name]; //add if directory if([NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) { //add [profiles addObject:name]; } } //sort for UI [profiles sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; //dbg msg os_log_debug(logHandle, "profiles: %{public}@", profiles); bail: return profiles; } //add new profile // and then set it to default -(BOOL)add:(NSString*)name preferences:(NSDictionary*)newPreferences { BOOL wasAdded = NO; NSError* error = nil; NSString *newProfilePath = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@ / %{public}@", __PRETTY_FUNCTION__, name, newPreferences); //create base profiles directory if needed if(YES != [NSFileManager.defaultManager fileExistsAtPath:self.directory]) { //create if(YES != [NSFileManager.defaultManager createDirectoryAtPath:self.directory withIntermediateDirectories:YES attributes:nil error:&error]) { //error os_log_error(logHandle, "ERROR: failed to create profiles directory '%{public}@': %{public}@", self.directory, error.localizedDescription); goto bail; } } //init path for new profile directory newProfilePath = [[[self.directory stringByAppendingPathComponent:name.lastPathComponent] stringByStandardizingPath] stringByResolvingSymlinksInPath]; //sanity check if(YES != [newProfilePath hasPrefix:[self.directory stringByAppendingString:@"/"]]) { //error os_log_error(logHandle, "ERROR: created path '%{public}@' isn't in the profile directory %{public}@", newProfilePath, self.directory); goto bail; } //if already exists, delete it if(YES == [NSFileManager.defaultManager fileExistsAtPath:newProfilePath]) { //remove install directory if(YES != [NSFileManager.defaultManager removeItemAtPath:newProfilePath error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to remove existing profile %{public}@ (error: %{public}@)", newProfilePath, error); } else { //dbg msg os_log_debug(logHandle, "removed existing profile %{public}@", newProfilePath); } } //create directory for new profile if(YES != [NSFileManager.defaultManager createDirectoryAtPath:newProfilePath withIntermediateDirectories:NO attributes:nil error:&error]) { //err msg os_log_error(logHandle, "ERROR: Failed to create new profile directory '%{public}@': %{public}@", newProfilePath, error.localizedDescription); goto bail; } //dbg msg os_log_debug(logHandle, "created profile directory: %{public}@", newProfilePath); //set as current [self set:newProfilePath]; //save new prefs // replacing all [preferences update:newPreferences replace:YES]; //clear out all rules @synchronized (rules) { [rules.rules removeAllObjects]; } //generate default rules if(YES != [rules generateDefaultRules]) { //err msg os_log_error(logHandle, "ERROR: failed to generate default rules"); //bail goto bail; } //save [rules save]; //reload rules [rules load]; //reload prefs [preferences load]; //happy wasAdded = YES; bail: return wasAdded; } //set current profile path in *default* prefs // note: can be called with nil to reset back to default profile -(void)set:(NSString*)profilePath { //set [preferences setCurrentProfile:profilePath]; return; } //delete a profile // delete folder matching name and reset to default if needed -(BOOL)delete:(NSString*)name { //flag BOOL wasDeleted = NO; //error NSError* error = nil; //current NSString* current = nil; //path NSString* profile = [self.directory stringByAppendingPathComponent:name]; //dbg msg os_log_debug(logHandle, "deleting profile directory: %{public}@", profile); //flag if(YES != [NSFileManager.defaultManager removeItemAtPath:profile error:&error]) { //err msg os_log_error(logHandle, "ERROR: Failed to delete profile directory '%{public}@': %{public}@", profile, error.localizedDescription); goto bail; } //dbg msg os_log_debug(logHandle, "deleted profile directory: %{public}@", profile); //get current current = [preferences getCurrentProfile]; //dbg msg os_log_debug(logHandle, "checking if %{public}@ matches current %{public}@", profile, current); //was current? if(YES == [profile isEqualToString:current]) { //dbg msg os_log_debug(logHandle, "'%{public}@' was current profile, so will reset back to default", profile); //unset path [self set:nil]; //reload rules [rules load]; //reload prefs [preferences load]; } //happy wasDeleted = YES; bail: return wasDeleted; } @end ================================================ FILE: LuLu/Extension/Rules.h ================================================ // // file: Rules.h // project: LuLu (launch daemon) // description: handles rules & actions such as add/delete (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #ifndef Rules_h #define Rules_h #import "Process.h" #import "XPCUserClient.h" @import OSLog; @import Foundation; @import NetworkExtension; @class Rule; @interface Rules : NSObject { } /* PROPERTIES */ //rules @property(nonatomic, retain)NSMutableDictionary* rules; //xpc client for talking to login item @property(nonatomic, retain)XPCUserClient* xpcUserClient; /* METHODS */ //prepare // first time? generate defaults rules // upgrade (v1.0)? convert to new format -(BOOL)prepare; //load from disk -(BOOL)load; //generate default rules -(BOOL)generateDefaultRules; //add a rule -(BOOL)add:(Rule*)rule save:(BOOL)save; //find (matching) rule -(Rule*)find:(Process*)process flow:(NEFilterSocketFlow*)flow csChange:(BOOL*)csChange; //disable (or re-enable) -(BOOL)toggleRule:(NSString*)key rule:(NSString*)uuid state:(NSNumber*)state; //delete rule -(BOOL)delete:(NSString*)key rule:(NSString*)uuid; //save -(BOOL)save; //import rules -(BOOL)import:(NSData*)rules userOnly:(BOOL)userOnly; //update an item's cs info // and also the cs info of all its rule -(void)updateCSInfo:(Process*)process; //cleanup rules -(NSUInteger)cleanup:(BOOL)full; @end #endif /* Rules_h */ ================================================ FILE: LuLu/Extension/Rules.m ================================================ // // file: Rules.m // project: LuLu (launch daemon) // description: handles rules & actions such as add/delete // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "Rule.h" #import "Rules.h" #import "Alerts.h" #import "consts.h" #import "Process.h" #import "utilities.h" #import "Preferences.h" //default systems 'allow' rules NSString* const DEFAULT_RULES[] = { @"/System/Library/PrivateFrameworks/ApplePushService.framework/apsd", @"/System/Library/PrivateFrameworks/AssistantServices.framework/Versions/A/Support/assistantd", @"/usr/sbin/automount", @"/System/Library/PrivateFrameworks/HelpData.framework/Versions/A/Resources/helpd", @"/usr/sbin/mDNSResponder", @"/sbin/mount_nfs", @"/usr/libexec/mount_url", @"/usr/sbin/ocspd", @"/usr/bin/sntp", @"/usr/libexec/trustd" }; /* format of rules dictionary of dictionaries key: signing id or item path (if unsigned) values: dictionary with: key: 'rules': value: array of rules key: 'csFlags': value: item's code signing flags */ /* GLOBALS */ //log handle extern os_log_t logHandle; //alerts obj extern Alerts* alerts; //prefs obj extern Preferences* preferences; @implementation Rules @synthesize rules; @synthesize xpcUserClient; //init method -(id)init { //init super self = [super init]; if(nil != self) { //alloc rules dictionary rules = [NSMutableDictionary dictionary]; //init XPC client xpcUserClient = [[XPCUserClient alloc] init]; } return self; } //prepare // first time? generate defaults rules // upgrade (v1.0)? convert to new format -(BOOL)prepare { //flag BOOL prepared = NO; //error NSError* error = nil; //old rule's file NSString* rulesFile_V1 = nil; //rule's file NSString* rulesFile = nil; //init path to old rule's file rulesFile_V1 = [INSTALL_DIRECTORY stringByAppendingPathComponent:RULES_FILE_V1]; //init path to rule's file rulesFile = [INSTALL_DIRECTORY stringByAppendingPathComponent:RULES_FILE]; //first check for old rules // ...and then upgrade to v2 format if(YES == [[NSFileManager defaultManager] fileExistsAtPath:rulesFile_V1]) { //dbg msg os_log_debug(logHandle, "found v1.0 rules: %{public}@", rulesFile_V1); //upgrade if(YES != [self upgrade:[NSDictionary dictionaryWithContentsOfFile:rulesFile_V1]]) { //err msg os_log_error(logHandle, "ERROR: failed to upgrade rules"); //but continue // will revert to generating default rules } //always delete rule v1.0 file if(YES != [NSFileManager.defaultManager removeItemAtPath:rulesFile_V1 error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to removed v1.0 rules: %{public}@", rulesFile_V1); } else os_log_debug(logHandle, "removed v1.0 rules: %{public}@", rulesFile_V1); } else os_log_debug(logHandle, "no v1.0 rules..."); //(still) no rules? // first time, so generate default rules if(YES != [[NSFileManager defaultManager] fileExistsAtPath:rulesFile]) { //dbg msg os_log_debug(logHandle, "no rules found"); //generate if(YES != [self generateDefaultRules]) { //err msg os_log_error(logHandle, "ERROR: failed to generate default rules"); //bail goto bail; } //save (generated rules) if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save (generated) rules"); //bail goto bail; } //dbg msg os_log_debug(logHandle, "done generating/saving default rules"); } //happy prepared = YES; bail: return prepared; } //upgrade rules from v1.0 // note: delete rules for paths the don't exist -(BOOL)upgrade:(NSDictionary*)rules_v1 { //flag BOOL upgraded = NO; //dbg msg os_log_debug(logHandle, "upgrading v1 rules..."); //upgrade all rules for(NSString* key in rules_v1) { //value NSDictionary* value = nil; //code signing info NSMutableDictionary* csInfo = nil; //info NSMutableDictionary* info = nil; //item doesn't exit? // just ignore it, and continue if(YES != [[NSFileManager defaultManager] fileExistsAtPath:key]) { //dbg msg os_log_debug(logHandle, "ignoring (v1) rule for %{public}@, as it no longer exists on disk", key); //ignore continue; } //extact value value = rules_v1[key]; //init info info = [NSMutableDictionary dictionary]; //add path info[KEY_PATH] = key; //add action info[KEY_ACTION] = value[KEY_ACTION]; //add type info[KEY_TYPE] = value[KEY_TYPE]; //extact cs info csInfo = [value[KEY_CS_INFO] mutableCopy]; //remove entitlments // not needed and can be loooong csInfo[KEY_CS_ENTITLEMENTS] = nil; //add cs info if(nil != csInfo) { //add info[KEY_CS_INFO] = csInfo; } //add // note: will save after loop if(YES != [self add:[[Rule alloc] init:info] save:NO]) { //err msg os_log_error(logHandle, "ERROR: failed to add rule"); //skip continue; } } //save if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save v1 -> v2 rules"); //bail goto bail; } //happy upgraded = YES; bail: return upgraded; } //get rule's path // either default, or one in current profile -(NSString*)getPath { //path NSString* path = nil; //current profile NSString* currentProfile = [preferences getCurrentProfile]; //init path to rule's file // which might be in a profile directory if(nil != currentProfile) { path = [currentProfile stringByAppendingPathComponent:RULES_FILE]; } //otherwise default else { //init w/ default path = [INSTALL_DIRECTORY stringByAppendingPathComponent:RULES_FILE]; } return path; } //load rules from disk // either default location, or from current profile -(BOOL)load { //result BOOL result = NO; //error NSError* error = nil; //rule's file NSString* rulesFile = nil; //archived rules NSData* archivedRules = nil; //item rules NSArray* itemRules = nil; //interval for expirations NSTimeInterval timeInterval = 0; //init path rulesFile = [self getPath]; //dbg msg os_log_debug(logHandle, "loading rules from: %{public}@", rulesFile); //(now) load archived rules from disk archivedRules = [NSData dataWithContentsOfFile:rulesFile]; if(nil == archivedRules) { //err msg os_log_error(logHandle, "ERROR: failed to load rules from %{public}@", rulesFile); //bail goto bail; } //unarchive self.rules = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithArray:@[[NSDictionary class], [NSArray class], [NSString class], [NSNumber class], [NSMutableSet class], [NSDate class], [Rule class]]] fromData:archivedRules error:&error]; if(nil == self.rules) { //err msg os_log_error(logHandle, "ERROR: failed to unarchive rules from %{public}@ (%{public}@)", RULES_FILE, error); //bail goto bail; } //make sure all item rules have a set for 'external' paths // older rules didn't use this, so let's make do it here manually for(NSString* key in self.rules) { //skip global rules if(YES == [key isEqualToString:VALUE_ANY]) { continue; } //skip directory rules // grab first/any rule and check if(YES == ((Rule*)[self.rules[key][KEY_RULES] firstObject]).isDirectory.boolValue) { continue; } //paths set nil? // alloc for paths if(nil == self.rules[key][KEY_PATHS]) { //alloc self.rules[key][KEY_PATHS] = [NSMutableSet set]; } } //setup deletion for any rules that have an expiration for(NSString* key in self.rules) { //item rules itemRules = self.rules[key][KEY_RULES]; //check each for(Rule* rule in itemRules) { //skip if(nil == rule.expiration) { continue; } //dbg msg os_log_debug(logHandle, "loaded rule has an expiration date set: %{public}@", rule.expiration); //interval timeInterval = [rule.expiration timeIntervalSinceNow]; //already expired? // just go ahead and delete if(timeInterval <= 0) { //delete [self delete:rule.key rule:rule.uuid]; } //setup dispatch to delete else { //dbg msg os_log_debug(logHandle, "setting up dispatch to delete one expiration is hit"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //dbg msg os_log_debug(logHandle, "rule expiration hit, will delete!"); //delete [self delete:rule.key rule:rule.uuid]; //tell user rules changed [alerts.xpcUserClient rulesChanged]; }); } } } //dbg msg os_log_debug(logHandle, "loaded %lu rules", (unsigned long)self.rules.count); //happy result = YES; bail: return result; } //generate default rules -(BOOL)generateDefaultRules { //flag BOOL generated = NO; //default binary NSString* defaultBinary = nil; //(rule) info NSMutableDictionary* info = nil; //binary Binary* binary = nil; //set cs flags SecCSFlags flags = kSecCSDefaultFlags | kSecCSCheckNestedCode | kSecCSDoNotValidateResources | kSecCSCheckAllArchitectures; //dbg msg os_log_debug(logHandle, "generating default rules"); //iterate overall default rule paths // generate binary obj/signing info for each for(NSUInteger i=0; i %{public}@", rule.key, rule); //sanity check if(nil == rule) { //err msg os_log_error(logHandle, "ERROR: can't add nil rule"); //bail goto bail; } //sync to access @synchronized(self.rules) { //new rule for item // need to init array for rules, paths, & cs info if(nil == self.rules[rule.key]) { //init self.rules[rule.key] = [NSMutableDictionary dictionary]; //init (proc) rules self.rules[rule.key][KEY_RULES] = [NSMutableArray array]; //add cs info if(nil != rule.csInfo) { //add self.rules[rule.key][KEY_CS_INFO] = rule.csInfo; } //init set for all paths self.rules[rule.key][KEY_PATHS] = [NSMutableSet set]; } //always add path (for UI) // note, insertion into a set is unique if(0 != rule.path.length) { //add [self.rules[rule.key][KEY_PATHS] addObject:rule.path]; } //(now) add rule [self.rules[rule.key][KEY_RULES] addObject:rule]; } //sync //handle expirations // setup dispatch to delete once it hit if(nil != rule.expiration) { //dbg msg os_log_debug(logHandle, "rule has an expiration date set: %{public}@", rule.expiration); timeInterval = [rule.expiration timeIntervalSinceNow]; if(timeInterval > 0) { //dbg msg os_log_debug(logHandle, "setting up dispatch to delete one expiration is hit"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //dbg msg os_log_debug(logHandle, "rule expiration hit, will delete!"); //delete [self delete:rule.key rule:rule.uuid]; //tell user rules changed [alerts.xpcUserClient rulesChanged]; }); } } //save if(YES == save) { //save if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save rules"); //bail goto bail; } //dbg msg os_log_debug(logHandle, "saved rule to disk"); } //no need to save else { //dbg msg os_log_debug(logHandle, "'save' not set (rule is temporary), so no need to write it out to disk"); } //happy added = YES; bail: return added; } //update an item's cs info // and also the cs info of all its rule -(void)updateCSInfo:(Process*)process { //dbg msg os_log_debug(logHandle, "updating code signing information for %{public}@ and its rules", process); //sync @synchronized(self.rules) { //update item's cs info self.rules[process.key][KEY_CS_INFO] = process.csInfo; //update also the cs info of all its rule for(Rule* rule in self.rules[process.key][KEY_RULES]) { //update rule.csInfo = process.csInfo; } } return; } //find (matching) rule -(Rule*)find:(Process*)process flow:(NEFilterSocketFlow*)flow csChange:(BOOL*)csChange { //rule's cs info NSDictionary* csInfo = nil; //matching rule Rule* matchingRule = nil; //global rules NSArray* globalRules = nil; //directory rules NSMutableArray* directoryRules = nil; //item's rules NSArray* itemRules = nil; //canidate rules NSMutableArray* candidateRules = nil; //flag(s) BOOL portAny = NO; BOOL endpointAny = NO; //any match // item has a '*:*' rule Rule* anyMatch = nil; //partial match // *:port or ip:* Rule* partialMatch = nil; //exact match Rule* exactMatch = nil; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //dbg msg os_log_debug(logHandle, "looking for rule for %{public}@ -> %{public}@", process.key, process.path); //sync to access @synchronized(self.rules) { //item rules itemRules = self.rules[process.key][KEY_RULES]; //extract cs info csInfo = self.rules[process.key][KEY_CS_INFO]; //cs info? // make sure it (still) matches if(nil != csInfo) { //mismatch? // ignore... if(YES != matchesCSInfo(process.csInfo, csInfo)) { //set *csChange = YES; //err msg os_log_error(logHandle, "ERROR: code signing mismatch: %{public}@ / %{public}@", process.csInfo, csInfo); //bail goto bail; } } //grab global rules globalRules = self.rules[VALUE_ANY][KEY_RULES]; //init directory rules directoryRules = [NSMutableArray array]; //add any directory rules // i.e. any rule that's '/*' for(NSString* key in self.rules) { //directory NSString* directory = nil; //directory rule? // grab first/any rule and check if(YES == ((Rule*)[self.rules[key][KEY_RULES] firstObject]).isDirectory.boolValue) { //init directory // ...by removing * directory = [key substringToIndex:(key.length-1)]; //does item fall within dir? if(YES == [process.path hasPrefix:directory]) { //add [directoryRules addObjectsFromArray:self.rules[key][KEY_RULES]]; } } } //no global, directory, nor item rules // bail, with no match so user is prompted if( (nil == itemRules) && (nil == globalRules) && (0 == directoryRules.count) ) { //no match goto bail; } //init candidate rules candidateRules = [NSMutableArray array]; //add global rules first if(nil != globalRules) [candidateRules addObject:globalRules]; //add directory rules next if(0 != directoryRules.count) [candidateRules addObject:directoryRules]; //add item's rules last... if(nil != itemRules) [candidateRules addObject:itemRules]; //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)flow.remoteEndpoint; //check each set of rules // set: global, directory, and/or item rules for(NSArray* rules in candidateRules) { //check all set of rules // note: * is a wildcard, meaning any match for(Rule* rule in rules) { //set flag: any port ('*') portAny = [rule.endpointPort isEqualToString:VALUE_ANY]; //set flag: any endpoint ('*', '0.0.0.0/0', or '::/0') endpointAny = [self matchAnyEndpoint:rule flow:flow]; //skip any disabled rule(s) if(0 != rule.isDisabled.intValue) { //dbg msg os_log_debug(logHandle, "skipping disabled rule match %{public}@", rule); //skip continue; } //checks for temp rules if(YES == [rule isTemporary]) { //process? // check process's pid matches rule's pid if( (nil != rule.pid) && (rule.pid.unsignedIntValue != process.pid) ) { //skip continue; } } //expiration? // double check if 'now' is after expiration if( (nil != rule.expiration) && ([[NSDate date] compare:rule.expiration] == NSOrderedDescending) ) { //err msg os_log_error(logHandle, "ERROR: rule %{public}@ has expired, should already have been removed", rule); //skip continue; } //match on any (addr) and any (port) if( (YES == portAny) && (YES == endpointAny) ) { //dbg msg os_log_debug(logHandle, "rule match: 'any' address and port"); //any anyMatch = rule; //next continue; } //port is any? // check for (partial) rule match: endpoint addr else if(YES == portAny) { //dbg msg os_log_debug(logHandle, "rule port is any ('*'), will check host/url"); //check endpoint host/url if(YES == [self endpointAddrMatch:flow rule:rule]) { //dbg msg os_log_debug(logHandle, "rule match: 'partial' (addr)"); //partial partialMatch = rule; //next continue; } } //endpoint addr is any? // check for (partial) rule match: endpoint port else if(YES == endpointAny) { //dbg msg os_log_debug(logHandle, "rule address is any ('*', '0.0.0.0/0', or '::/0'), will check port"); //addr is any //so check the port if(YES == [rule.endpointPort isEqualToString:remoteEndpoint.port]) { //dbg msg os_log_debug(logHandle, "rule match: 'partial' (port)"); //partial partialMatch = rule; //next continue; } } //addr and port both set (not '*') // check that both endpoint addr and port match else { //dbg msg os_log_debug(logHandle, "address and port set (%{public}@:%{public}@), will check both for match", rule.endpointAddr, rule.endpointPort); //match? if( (YES == [self endpointAddrMatch:flow rule:rule]) && (YES == [rule.endpointPort isEqualToString:remoteEndpoint.port]) ) { //dbg msg os_log_debug(logHandle, "rule match: 'exact' address and port"); //exact exactMatch = rule; //next continue; } } } //all rule set } //all candidate rules //extact match? if(nil != exactMatch) matchingRule = exactMatch; //partial match? else if (nil != partialMatch) matchingRule = partialMatch; //any match? else if (nil != anyMatch) matchingRule = anyMatch; }//sync bail: return matchingRule; } //check if endpoint addr matches "any" // either '*' or (IPv4) '0.0.0.0/0' or (IPv6) '::/0' -(BOOL)matchAnyEndpoint:(Rule*)rule flow:(NEFilterSocketFlow*)flow { //flag BOOL matchesAny = NO; //first check any '*' if([rule.endpointAddr isEqualToString:VALUE_ANY]) { //dbg msg os_log_debug(logHandle, "rule match: 'any' ('*')"); matchesAny = YES; goto bail; } //check for IPV4 -> '0.0.0.0/0' if( (AF_INET == flow.socketFamily) && ([rule.endpointAddr isEqualToString:@"0.0.0.0/0"]) ) { //dbg msg os_log_debug(logHandle, "rule match: 'any' (IPv4: '0.0.0.0/0')"); matchesAny = YES; goto bail; } //check for IPV6 -> '::/0' if( (AF_INET6 == flow.socketFamily) && ([rule.endpointAddr isEqualToString:@"::/0"]) ) { //dbg msg os_log_debug(logHandle, "rule match: 'any' (IPv6: '::/0')"); matchesAny = YES; goto bail; } bail: return matchesAny; } //check if endpoint host or url matches // extra logic for checking/applying regex, etc... -(BOOL)endpointAddrMatch:(NEFilterSocketFlow*)flow rule:(Rule*)rule { //match BOOL isMatch = NO; //error NSError* error = nil; //endpoint url/hosts NSMutableArray* endpointNames = nil; //endpoint regex NSRegularExpression* endpointAddrRegex = nil; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //dbg msg os_log_debug(logHandle, "%s", __PRETTY_FUNCTION__); //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)flow.remoteEndpoint; //init endpoint names endpointNames = [NSMutableArray array]; //add url // and just host (as this is what is shown in alert) if(nil != flow.URL.absoluteString) { //add full url [endpointNames addObject:flow.URL.absoluteString]; //add host [endpointNames addObject:flow.URL.host]; } //add host name if(nil != remoteEndpoint.hostname) { //add [endpointNames addObject:remoteEndpoint.hostname]; } //macOS 11+? // add remote host name if(@available(macOS 11, *)) { //add remote host name if(nil != flow.remoteHostname) { //add [endpointNames addObject:flow.remoteHostname]; } } //dbg msg os_log_debug(logHandle, "checking rule's endpoint address (%{public}@) and rule's endpoint host %{public}@ against %{public}@", rule.endpointAddr, rule.endpointHost, endpointNames); //endpoint addr a regex? // init regex and check for match if(YES == rule.isEndpointAddrRegex) { //dbg msg os_log_debug(logHandle, "rule's endpoint address is a regex..."); //init regex endpointAddrRegex = [NSRegularExpression regularExpressionWithPattern:rule.endpointAddr options:0 error:&error]; if(nil == endpointAddrRegex) { //err msg os_log_error(logHandle, "ERROR: failed to created regex from %{public}@ (error: %{public}@)", rule.endpointAddr, error); //bail goto bail; } //check each for(NSString* endpointName in endpointNames) { //match? if(0 != [endpointAddrRegex numberOfMatchesInString:endpointName options:0 range:NSMakeRange(0, endpointName.length)]) { //dbg msg os_log_debug(logHandle, "rule match: regex on %{public}@", endpointName); //match isMatch = YES; //bail goto bail; } } } //not regex // check rule's endpoint address and host for (exact) match else { //check each for(NSString* endpointName in endpointNames) { //dbg msg os_log_debug(logHandle, "checking %{public}@ vs. %{public}@", rule.endpointAddr, endpointName); //check against rule's endpoint address if(NSOrderedSame == [rule.endpointAddr caseInsensitiveCompare:endpointName]) { //dbg msg os_log_debug(logHandle, "rule match (endpoint address): %{public}@", endpointName); //match isMatch = YES; goto bail; } //(also) check against rule's endpoint host if( (nil != rule.endpointHost) && (NSOrderedSame == [rule.endpointHost caseInsensitiveCompare:endpointName]) ) { //dbg msg os_log_debug(logHandle, "rule match: (endpoint host) %{public}@", endpointName); //match isMatch = YES; goto bail; } } } bail: return isMatch; } //toggle rule // either disable, or (re)enable -(BOOL)toggleRule:(NSString*)key rule:(NSString*)uuid state:(NSNumber*)state { //result BOOL result = NO; //dbg msg os_log_debug(logHandle, "toggling rule, key: %{public}@, rule id: %{public}@", key, uuid); //sync to access @synchronized(self.rules) { //no uuid // set all (process') rules to specified state if(nil == uuid) { //toggle all for(Rule* rule in self.rules[key][KEY_RULES]) { //enable if(RULE_TOGGLE_STATE_ENABLE == state.intValue) { rule.isDisabled = nil; } //disable else { rule.isDisabled = @YES; } } //happy result = YES; //done goto bail; } //find matching rule [self.rules[key][KEY_RULES] enumerateObjectsUsingBlock:^(Rule* currentRule, NSUInteger index, BOOL* stop) { //is match? if(YES == [currentRule.uuid isEqualToString:uuid]) { //enable if(RULE_TOGGLE_STATE_ENABLE == state.intValue) { currentRule.isDisabled = nil; } //disable else { currentRule.isDisabled = @YES; } //done *stop = YES; } }]; } //sync //happy result = YES; bail: //always save to disk if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save (toggled) rules"); //not happy result = NO; } return result; } //delete rule -(BOOL)delete:(NSString*)key rule:(NSString*)uuid { //result BOOL result = NO; //rule index __block NSUInteger ruleIndex = -1; //dbg msg os_log_debug(logHandle, "deleting rule, key: %{public}@, rule id: %{public}@", key, uuid); //sync to access @synchronized(self.rules) { //no uuid // delete all (process') rules if(nil == uuid) { //remove [self.rules removeObjectForKey:key]; //happy result = YES; //done goto bail; } //find matching rule [self.rules[key][KEY_RULES] enumerateObjectsUsingBlock:^(Rule* currentRule, NSUInteger index, BOOL* stop) { //is match? if(YES == [currentRule.uuid isEqualToString:uuid]) { //save index ruleIndex = index; //stop *stop = YES; } }]; //dbg msg os_log_debug(logHandle, "found rule at index: %lu", (unsigned long)ruleIndex); //remove if(-1 != ruleIndex) { //remove [self.rules[key][KEY_RULES] removeObjectAtIndex:ruleIndex]; //last (item) rule? if(0 == ((NSMutableArray*)self.rules[key][KEY_RULES]).count) { //dbg msg os_log_debug(logHandle, "rule was only/last one for %{public}@, so removing item entry", key); //remove process [self.rules removeObjectForKey:key]; } } } //sync //happy result = YES; bail: //always save to disk if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save (updated) rules"); //not happy result = NO; } return result; } //save to disk // note: temporary rules are ignored -(BOOL)save { //result BOOL result = NO; //error NSError* error = nil; //rule's file NSString* rulesFile = nil; //persistent rules NSMutableDictionary* persistentRules = nil; //archived rules NSData* archivedRules = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //init persistentRules = [NSMutableDictionary dictionary]; //init path rulesFile = [self getPath]; //dbg msg os_log_debug(logHandle, "saving (non-temp) rules to %{public}@", rulesFile); //sync to save @synchronized(self) { //generate list of non-temp rules // these are the ones we'll write out for(NSString* key in self.rules.allKeys) { //item's rules NSMutableArray* itemRules = nil; //init itemRules = [NSMutableArray array]; //add only non-temporary rules for(Rule* itemRule in self.rules[key][KEY_RULES]) { //temp? if(YES == [itemRule isTemporary]) { //skip continue; } //add [itemRules addObject:itemRule]; } //any non-temp rules? // add to rules we're going to write out if(0 != itemRules.count) { //start w/ empty dictionary persistentRules[key] = [NSMutableDictionary dictionary]; //add (non-temp) rules persistentRules[key][KEY_RULES] = itemRules; //add cs info if(nil != self.rules[key][KEY_CS_INFO]) { //add persistentRules[key][KEY_CS_INFO] = self.rules[key][KEY_CS_INFO]; } //add paths info if(nil != self.rules[key][KEY_PATHS]) { //add persistentRules[key][KEY_PATHS] = self.rules[key][KEY_PATHS]; } } } //serialize archivedRules = [NSKeyedArchiver archivedDataWithRootObject:persistentRules requiringSecureCoding:YES error:&error]; if(nil == archivedRules) { //err msg os_log_error(logHandle, "ERROR: failed to serialize rules: %{public}@", error); //bail goto bail; } //dbg msg os_log_debug(logHandle, "serialized rules"); //write out rules if(YES != [archivedRules writeToFile:rulesFile atomically:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to save archived rules to: %{public}@", rulesFile); //bail goto bail; } } //sync //happy result = YES; bail: return result; } //import rules -(BOOL)import:(NSData*)importedRules userOnly:(BOOL)userOnly { //flag BOOL result = NO; //unserialized rules NSDictionary* unarchivedRules = nil; //error NSError* error = nil; //dbg msg os_log_debug(logHandle, "method '%s' invoked with %{public}@", __PRETTY_FUNCTION__, importedRules); //sanity check if(YES != [importedRules isKindOfClass:[NSData class]]) { //err msg os_log_error(logHandle, "ERROR: imported rules are not a NSData"); goto bail; } //unarchive unarchivedRules = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithArray:@[[NSDictionary class], [NSArray class], [NSString class], [NSNumber class], [NSMutableSet class], [NSDate class], [Rule class]]] fromData:importedRules error:&error]; //error? if( (nil != error) || (nil == unarchivedRules) ) { //err msg os_log_error(logHandle, "ERROR: failed to unarchive (imported) rules (error: %{public}@)", error); goto bail; } //dbg msg os_log_debug(logHandle, "unarchived (imported) %lu rules", (unsigned long)unarchivedRules.count); //update @synchronized (self) { //full update if(!userOnly) { //dbg msg os_log_debug(logHandle, "full import"); //update all self.rules = [unarchivedRules mutableCopy]; } //user-only update else { //dbg msg os_log_debug(logHandle, "partial (user-created only) import"); //first: remove all existing user-created rules for(NSString* key in self.rules.allKeys) { //get rules for key NSMutableArray* existingRules = self.rules[key][KEY_RULES]; //remove user-created rules [existingRules filterUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(Rule* rule, NSDictionary* bindings) { return (rule.type.intValue != RULE_TYPE_USER); }] ]; //no rules left? // remove entire key if(0 == existingRules.count) { [self.rules removeObjectForKey:key]; } } //second: merge in imported user rules for(NSString* key in unarchivedRules) { //key exists? // (has non-user rules) if(nil != self.rules[key]){ //append imported rules [self.rules[key][KEY_RULES] addObjectsFromArray:unarchivedRules[key][KEY_RULES]]; } //new key else{ //add entire entry self.rules[key] = [unarchivedRules[key] mutableCopy]; } } } } //save if(YES != [self save]) { //err msg os_log_error(logHandle, "ERROR: failed to save (imported) rules"); //bail goto bail; } //dbg msg os_log_debug(logHandle, "saved (imported) rules"); //happy result = YES; bail: return result; } //cleanup // a) rule expired // a) path was deleted // b) point to non-existent processes (temp rule) -(NSUInteger)cleanup:(BOOL)full { //count NSUInteger deletedRules = 0; //rules to delete NSMutableArray* rules2Delete = nil; //dbg msg os_log_debug(logHandle, "cleaning up rules (full?: %d)", full); //alloc rules2Delete = [NSMutableArray array]; //sync to access @synchronized(self.rules) { //gather all rules for(NSString* key in self.rules.allKeys) { //paths NSArray* paths = nil; //path NSString* path = nil; //rules for item NSArray* rules = nil; //(first) rule Rule* rule = nil; //flag BOOL allDeleted = YES; //interval for expiration check NSTimeInterval timeInterval = 0; //extract 'external' paths paths = self.rules[key][KEY_PATHS]; //extract rules for item rules = self.rules[key][KEY_RULES]; //extract (first) rule rule = rules.firstObject; //skip global rules if(YES == rule.isGlobal.boolValue) { //skip continue; } //only do path checks on full cleanup if(full) { //directory rule? // check if directory has been deleted if(YES == rule.isDirectory.boolValue) { //directory rules end in /* // so first remove the trailing '*' path = [rule.path substringToIndex:rule.path.length - 1]; //was directory deleted? if(YES != [NSFileManager.defaultManager fileExistsAtPath:path]) { //dbg msg os_log_debug(logHandle, "%{public}@ is gone, will delete directory rule", path); //add to list [rules2Delete addObject:rule]; } //next continue; } //for (normal) item rules // first set flag if all 'external' paths have been deleted for(NSString* path in paths) { //path still there? if(YES == [NSFileManager.defaultManager fileExistsAtPath:path]) { //toggle allDeleted = NO; //done break; } } } //check each rule's 'internal' path // and expiration, and process id (temp rules) for(Rule* rule in rules) { //only do path checks on full cleanup if(full) { //was path deleted? // and all 'external' paths too? if( (YES == allDeleted) && (YES != [NSFileManager.defaultManager fileExistsAtPath:rule.path])) { //dbg msg os_log_debug(logHandle, "%{public}@ is gone - will delete rule", rule.path); //add to list [rules2Delete addObject:rule]; //next continue; } } //did process (for temp rule) exit? if( (YES == [rule isTemporary]) && (YES != isAlive(rule.pid.intValue)) ) { //dbg msg os_log_debug(logHandle, "process-level (temporary) rule's process (%@) has exited - will delete rule", rule.pid); //add to list [rules2Delete addObject:rule]; //next continue; } //did rule expire? if(nil != rule.expiration) { //compute interval // and check if it expired timeInterval = [rule.expiration timeIntervalSinceNow]; if(timeInterval <= 0) { //dbg msg os_log_debug(logHandle, "rule's expiration has hit (%@) - will delete rule", rule.expiration); //add to list [rules2Delete addObject:rule]; //next continue; } } } } } //save count deletedRules = rules2Delete.count; //now delete each for(Rule* rule in rules2Delete) { //delete [self delete:rule.key rule:rule.uuid]; } //dbg msg os_log_debug(logHandle, "cleaned up/deleted %ld rules", (long)deletedRules); return deletedRules; } @end ================================================ FILE: LuLu/Extension/XPCDaemon.h ================================================ // // file: XPCDaemon.h // project: lulu (launch daemon) // description: interface for XPC methods, invoked by user (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import OSLog; @import Foundation; #import "XPCDaemonProto.h" @interface XPCDaemon : NSObject { } /* PROPERTIES */ @end ================================================ FILE: LuLu/Extension/XPCDaemon.m ================================================ // // file: XPCDaemon.m // project: lulu (launch daemon) // description: interface for XPC methods, invoked by user // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "Rule.h" #import "Rules.h" #import "Alerts.h" #import "consts.h" #import "Profiles.h" #import "XPCDaemon.h" #import "utilities.h" #import "Preferences.h" //global rules obj extern Rules* rules; //global alerts obj extern Alerts* alerts; //global profiles obj extern Profiles* profiles; //global prefs obj extern Preferences* preferences; //global log handle extern os_log_t logHandle; @implementation XPCDaemon //send preferences to the client -(void)getPreferences:(void (^)(NSDictionary*))reply { //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //reply w/ prefs reply(preferences.preferences); return; } //update preferences // note: sends full preferences back to the client -(void)updatePreferences:(NSDictionary*)updates reply:(void (^)(NSDictionary*))reply { //dbg msg os_log_debug(logHandle, "XPC request: '%s' (%{public}@)", __PRETTY_FUNCTION__, updates); //call into prefs obj if(YES != [preferences update:updates replace:NO]) { //err msg os_log_error(logHandle, "ERROR: failed to updates to preferences"); } //reply w/ prefs reply(preferences.preferences); return; } //send rules to the client -(void)getRules:(void (^)(NSData*))reply { //archived rules NSData* archivedRules = nil; //error NSError* error = nil; //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //archive rules archivedRules = [NSKeyedArchiver archivedDataWithRootObject:rules.rules requiringSecureCoding:YES error:&error]; if(nil == archivedRules) { //err msg os_log_error(logHandle, "ERROR: failed to archive rules: %{public}@", error); } else os_log_debug(logHandle, "archived %lu rules, and sending to user...", (unsigned long)rules.rules.count); //reply w/ rules reply(archivedRules); return; } //add a rule -(void)addRule:(NSDictionary*)info { //binary obj Binary* binary = nil; //rule info NSMutableDictionary* ruleInfo = nil; //default cs flags SecCSFlags flags = kSecCSDefaultFlags | kSecCSCheckNestedCode | kSecCSDoNotValidateResources | kSecCSCheckAllArchitectures; //dbg msg os_log_debug(logHandle, "XPC request: '%s' with info: %{public}@", __PRETTY_FUNCTION__, info); //make copy ruleInfo = [info mutableCopy]; //non-specific path // init binary and cs info if(YES != [info[KEY_PATH] hasSuffix:VALUE_ANY]) { //init binary obj w/ path binary = [[Binary alloc] init:info[KEY_PATH]]; if(nil == binary) { //err msg os_log_error(logHandle, "ERROR: failed init binary object for %@", info[KEY_PATH]); //bail goto bail; } //generate cs info [binary generateSigningInfo:flags]; //add any code signing info if(nil != binary.csInfo) { //add ruleInfo[KEY_CS_INFO] = binary.csInfo; } } //create and add rule if(YES != [rules add:[[Rule alloc] init:ruleInfo] save:YES]) { //err msg os_log_error(logHandle, "ERROR: failed to add rule for %{public}@", ruleInfo[KEY_PATH]); //bail goto bail; } //dbg msg os_log_debug(logHandle, "added rule"); bail: return; } //disable (or re-enable) rule -(void)toggleRule:(NSString*)key rule:(NSString*)uuid state:(NSNumber*)state { //dbg msg os_log_debug(logHandle, "XPC request: '%s' with key: %{public}@, rule id: %{public}@", __PRETTY_FUNCTION__, key, uuid); //toggle if(YES != [rules toggleRule:key rule:uuid state:state]) { //err msg os_log_error(logHandle, "ERROR: failed to toggle rule"); //bail goto bail; } //dbg msg os_log_debug(logHandle, "toggled rule"); bail: return; } //delete rule -(void)deleteRule:(NSString*)key rule:(NSString*)uuid { //dbg msg os_log_debug(logHandle, "XPC request: '%s' with key: %{public}@, rule id: %{public}@", __PRETTY_FUNCTION__, key, uuid); //delete rule if(YES != [rules delete:key rule:uuid]) { //err msg os_log_error(logHandle, "ERROR: failed to delete rule"); //bail goto bail; } //dbg msg os_log_debug(logHandle, "deleted rule"); bail: return; } //import rules -(void)importRules:(NSData*)importedRules userOnly:(BOOL)userOnly result:(void (^)(BOOL))reply { //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //import rules reply([rules import:importedRules userOnly:userOnly]); return; } //cleanup rules -(void)cleanupRules:(BOOL)full reply:(void (^)(NSInteger))reply { //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //cleanup rules reply([rules cleanup:full]); return; } //uninstall -(void)uninstall:(void (^)(BOOL))reply { //flag BOOL uninstalled = NO; //directory NSString* path = nil; //error NSError* error = nil; //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //init path w/ install dir path = INSTALL_DIRECTORY; //remove install directory if(YES != [NSFileManager.defaultManager removeItemAtPath:path error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to remove %{public}@ (error: %{public}@)", path, error); } else { //dbg msg os_log_debug(logHandle, "removed %{public}@", path); //happy uninstalled = YES; } //up to Obj-See's install dir path = [path stringByDeletingLastPathComponent]; //no other Obj-See tools? // remove the Obj-See directory too if( (0 == [[NSFileManager.defaultManager contentsOfDirectoryAtPath:path error:&error] count]) && (nil == error) ) { //remove if(YES != [NSFileManager.defaultManager removeItemAtPath:path error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to delete %{public}@ (error: %{public}@)", path, error); } else { //dbg msg os_log_debug(logHandle, "removed %{public}@", path); //happy uninstalled = YES; } } //return result reply(uninstalled); return; } //get current profile *name* -(void)getCurrentProfile:(void (^)(NSString*))reply { //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //reply reply([[preferences getCurrentProfile] lastPathComponent]); return; } //get list of profile names -(void)getProfiles:(void (^)(NSArray*))reply { //dbg msg os_log_debug(logHandle, "XPC request: '%s'", __PRETTY_FUNCTION__); //reply w/ prefs // return immutable copy reply([profiles enumerate].copy); return; } //add profile -(void)addProfile:(NSString*)name preferences:(NSDictionary*)preferences reply:(void (^)(BOOL))reply { //flag BOOL wasAdded = NO; //dbg msg os_log_debug(logHandle, "XPC request: '%s' with %{public}@", __PRETTY_FUNCTION__, name); //create and add profile if(YES != [profiles add:name preferences:preferences]) { //err msg os_log_error(logHandle, "ERROR: failed to add new profile '%{public}@'", name); //bail goto bail; } //happy wasAdded = YES; //dbg msg os_log_debug(logHandle, "added new profile '%{public}@'", name); bail: reply(wasAdded); return; } //set profile -(void)setProfile:(NSString*)name reply:(void (^)(BOOL))reply { //flag BOOL wasSet = NO; //full path NSString* newProfilePath = nil; //dbg msg os_log_debug(logHandle, "XPC request: '%s' with name: %{public}@", __PRETTY_FUNCTION__, name); //new profile? if(nil != name) { //init path for new profile directory newProfilePath = [profiles.directory stringByAppendingPathComponent:name]; } //set [profiles set:newProfilePath]; //reload rules [rules load]; //tell user rules changed // ...in case rule's window need refreshing [alerts.xpcUserClient rulesChanged]; //reload prefs [preferences load]; //happy wasSet = YES; //dbg msg os_log_debug(logHandle, "set profile to %{public}@", newProfilePath ? newProfilePath : @"Default"); reply(wasSet); return; } //delete profile -(void)deleteProfile:(NSString*)name reply:(void (^)(BOOL))reply { //flag BOOL wasDeleted = NO; //dbg msg os_log_debug(logHandle, "XPC request: '%s' with name: %{public}@", __PRETTY_FUNCTION__, name); //sanity check if(nil == name) { //err msg os_log_error(logHandle, "ERROR: profile name is nil, so ignoring..."); goto bail; } //delete profile if(YES != [profiles delete:name]) { //err msg os_log_error(logHandle, "ERROR: failed to delete profile"); goto bail; } //happy wasDeleted = YES; //dbg msg os_log_debug(logHandle, "deleted profile %{public}@", name); bail: reply(wasDeleted); return; } @end ================================================ FILE: LuLu/Extension/XPCListener.h ================================================ // // file: XPCListener // project: lulu (launch daemon) // description: XPC listener for connections for user components (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // @import OSLog; @import Foundation; #import "XPCDaemonProto.h" //function def OSStatus SecTaskValidateForRequirement(SecTaskRef task, CFStringRef requirement); @interface XPCListener : NSObject { } /* PROPERTIES */ //XPC listener @property(nonatomic, retain)NSXPCListener* listener; //XPC connection for login item @property(weak)NSXPCConnection* client; @end ================================================ FILE: LuLu/Extension/XPCListener.m ================================================ // // file: XPCListener.m // project: lulu (launch daemon) // description: XPC listener for connections for user components // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "Rule.h" #import "Rules.h" #import "Alerts.h" #import "utilities.h" #import "XPCListener.h" #import "XPCDaemon.h" #import "XPCUserProto.h" #import "XPCDaemonProto.h" @import OSLog; #import /* GLOBALS */ //alerts extern Alerts* alerts; //interface for 'extension' to NSXPCConnection // allows us to access the 'private' auditToken iVar @interface ExtendedNSXPCConnection : NSXPCConnection { //private iVar audit_token_t auditToken; } //private iVar @property audit_token_t auditToken; @end //implementation for 'extension' to NSXPCConnection // allows us to access the 'private' auditToken iVar @implementation ExtendedNSXPCConnection //private iVar @synthesize auditToken; @end //global logging handle extern os_log_t logHandle; @implementation XPCListener @synthesize client; @synthesize listener; //init // create XPC listener -(id)init { //code signing requirement NSString* requirement = nil; //init super self = [super init]; if(nil != self) { //init listener listener = [[NSXPCListener alloc] initWithMachServiceName:DAEMON_MACH_SERVICE]; //macOS 13+ // set code signing requirement for clients via 'setConnectionCodeSigningRequirement' if(@available(macOS 13.0, *)) { //init requirement // LuLu client, v2.0+ requirement = [NSString stringWithFormat:@"anchor apple generic and identifier \"%@\" and certificate leaf [subject.CN] = \"%@\" and info [CFBundleShortVersionString] >= \"2.0.0\"", APP_ID, SIGNING_AUTH]; //set requirement [self.listener setConnectionCodeSigningRequirement:requirement]; //dbg msg os_log_debug(logHandle, "set XPC requirement %@", requirement); } //dbg msg os_log_debug(logHandle, "created mach service %@", DAEMON_MACH_SERVICE); //set delegate self.listener.delegate = self; //ready to accept connections [self.listener resume]; } return self; } #pragma mark - #pragma mark NSXPCConnection method overrides //automatically invoked // allows NSXPCListener to configure/accept/resume a new incoming NSXPCConnection // shoutout to writeup: https://blog.obdev.at/what-we-have-learned-from-a-vulnerability -(BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { //flag BOOL shouldAccept = NO; //status OSStatus status = !errSecSuccess; //audit token audit_token_t auditToken = {0}; //task ref SecTaskRef taskRef = 0; //code ref SecCodeRef codeRef = NULL; //code signing info CFDictionaryRef csInfo = NULL; //cs flags uint32_t csFlags = 0; //signing req string (main app) NSString* requirement = nil; //extract audit token auditToken = ((ExtendedNSXPCConnection*)newConnection).auditToken; //dbg msg os_log_debug(logHandle, "received request to connect to XPC interface from: (%d)%{public}@", audit_token_to_pid(auditToken), getProcessPath(audit_token_to_pid(auditToken))); //obtain dynamic code ref status = SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(@{(__bridge NSString *)kSecGuestAttributeAudit : [NSData dataWithBytes:&auditToken length:sizeof(audit_token_t)]}), kSecCSDefaultFlags, &codeRef); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCopyGuestWithAttributes' failed with': %#x", status); //bail goto bail; } //validate code status = SecCodeCheckValidity(codeRef, kSecCSDefaultFlags, NULL); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCheckValidity' failed with': %#x", status); //bail goto bail; } //get code signing info status = SecCodeCopySigningInformation(codeRef, kSecCSDynamicInformation, &csInfo); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCopySigningInformation' failed with': %#x", status); //bail goto bail; } //dbg msg os_log_debug(logHandle, "client's code signing info: %{public}@", csInfo); //extract flags csFlags = [((__bridge NSDictionary *)csInfo)[(__bridge NSString *)kSecCodeInfoStatus] unsignedIntValue]; //dbg msg os_log_debug(logHandle, "client code signing flags: %#x", csFlags); //gotta have hardened runtime if( !(CS_VALID & csFlags) && !(CS_RUNTIME & csFlags) ) { //err msg os_log_error(logHandle, "ERROR: invalid code signing flags: %#x", csFlags); //bail goto bail; } //dbg msg os_log_debug(logHandle, "client code signing flags, ok (includes 'CS_RUNTIME')"); //init signing req requirement = [NSString stringWithFormat:@"anchor apple generic and identifier \"%@\" and certificate leaf [subject.CN] = \"%@\"", APP_ID, SIGNING_AUTH]; //step 1: create task ref // uses NSXPCConnection's (private) 'auditToken' iVar taskRef = SecTaskCreateWithAuditToken(NULL, ((ExtendedNSXPCConnection*)newConnection).auditToken); if(NULL == taskRef) { //bail goto bail; } //step 2: validate // check that client is signed with Objective-See's and it's LuLu if(errSecSuccess != (status = SecTaskValidateForRequirement(taskRef, (__bridge CFStringRef)(requirement)))) { //err msg os_log_error(logHandle, "ERROR: failed with validate client (error: %#x/%d)", status, status); //bail goto bail; } //dbg msg os_log_debug(logHandle, "client code signing information, ok"); //set the interface that the exported object implements newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCDaemonProtocol)]; //set object exported by connection newConnection.exportedObject = [[XPCDaemon alloc] init]; //set type of remote object // user (login item/main app) will set this object newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol: @protocol(XPCUserProtocol)]; //set interruption handler [newConnection setInterruptionHandler:^{ //dbg msg os_log_debug(logHandle, "XPC 'interruptionHandler' method invoked"); //unset user alerts.consoleUser = nil; }]; //set invalidation handler [newConnection setInvalidationHandler:^{ //dbg msg os_log_debug(logHandle, "XPC 'invalidationHandler' method invoked"); //unset user alerts.consoleUser = nil; }]; //save self.client = newConnection; //and set user alerts.consoleUser = getConsoleUser(); //resume [newConnection resume]; //dbg msg os_log_debug(logHandle, "allowing XPC connection from client (pid: %d, user: %{public}@)", audit_token_to_pid(auditToken), alerts.consoleUser); //happy shouldAccept = YES; bail: //release task ref object if(NULL != taskRef) { //release CFRelease(taskRef); taskRef = NULL; } //free cs info if(NULL != csInfo) { //free CFRelease(csInfo); csInfo = NULL; } //free code ref if(NULL != codeRef) { //free CFRelease(codeRef); codeRef = NULL; } return shouldAccept; } @end ================================================ FILE: LuLu/Extension/XPCUserClient.h ================================================ // // file: XPCUserClient.h // project: lulu (launch daemon) // description: talk to the user, via XPC (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // @import OSLog; @import Foundation; #import "XPCUserProto.h" @interface XPCUserClient : NSObject { } /* PROPERTIES */ /* METHODS */ //deliver alert to user // note: this is synchronous, so errors can be detected -(BOOL)deliverAlert:(NSDictionary*)alert reply:(void (^)(NSDictionary*))reply; //inform user rules have changed -(void)rulesChanged; @end ================================================ FILE: LuLu/Extension/XPCUserClient.m ================================================ // // file: XPCUserClient.m // project: lulu (launch daemon) // description: talk to the user, via XPC (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // #import "Rules.h" #import "Alerts.h" #import "consts.h" #import "XPCListener.h" #import "XPCUserClient.h" /* GLOBALS */ //xpc connection extern XPCListener* xpcListener; //log handle extern os_log_t logHandle; @implementation XPCUserClient //deliver alert to user -(BOOL)deliverAlert:(NSDictionary*)alert reply:(void (^)(NSDictionary*))reply { //flag __block BOOL xpcError = NO; //sanity check // no client connection? if(nil == xpcListener.client) { //dbg msg os_log_debug(logHandle, "no client is connected, alert will not be delivered"); //set error xpcError = YES; //bail //goto bail; } else { //dbg msg os_log_debug(logHandle, "invoking user XPC method: 'alertShow:reply:'"); //send to user [[xpcListener.client remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute daemon XPC method '%s' (error: %{public}@)", __PRETTY_FUNCTION__, proxyError); //set error xpcError = YES; }] alertShow:alert reply:^(NSDictionary* userReply) { //dbg msg os_log_debug(logHandle, "reply: %{public}@", alert); //respond reply(userReply); }]; } bail: return !xpcError; } //inform user rules have changed -(void)rulesChanged { //dbg msg os_log_debug(logHandle, "invoking user XPC method, '%s'", __PRETTY_FUNCTION__); //no client? // no need to do anything... if(nil == xpcListener.client) { //bail goto bail; } //send to user (login item) to display [[xpcListener.client remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { //err msg os_log_error(logHandle, "ERROR: failed to execute 'rulesChanged' method on launch daemon (error: %{public}@)", proxyError); }] rulesChanged]; bail: return; } @end ================================================ FILE: LuLu/Extension/main.h ================================================ // // file: main.h // project: lulu (launch daemon) // description: main (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // #import "Rules.h" #import "Alerts.h" #import "consts.h" #import "Profiles.h" #import "utilities.h" #import "Preferences.h" #import "XPCListener.h" #import "BlockOrAllowList.h" #ifndef main_h #define main_h //GLOBALS //rules obj Rules* rules = nil; //alerts obj Alerts* alerts = nil; //allow list BlockOrAllowList* allowList = nil; //block list BlockOrAllowList* blockList = nil; //XPC listener obj XPCListener* xpcListener = nil; //prefs obj Preferences* preferences = nil; //profile obj Profiles* profiles = nil; //dispatch source for SIGTERM dispatch_source_t dispatchSource = nil; /* FUNCTIONS */ //init a handler for SIGTERM // can perform actions such as disabling firewall and closing logging void register4Shutdown(void); //launch daemon should only be unloaded if box is shutting down // so handle things like telling kext to disable & unregister, de-init logging, etc void goodbye(void); #endif /* main_h */ ================================================ FILE: LuLu/Extension/main.m ================================================ // // main.m // Extension // // Created by Patrick Wardle on 8/1/20. // Copyright (c) 2020 Objective-See. All rights reserved. // //FOR LOGGING: // % log stream --level debug --predicate="subsystem='com.objective-see.lulu'" #import "main.h" @import OSLog; @import Foundation; @import NetworkExtension; /* GLOBALS */ //log handle os_log_t logHandle = nil; //main int main(int argc, char *argv[]) { //pool @autoreleasepool { //init log logHandle = os_log_create(BUNDLE_ID, "extension"); //dbg msg os_log_debug(logHandle, "started: %{public}@ (pid: %d / uid: %d)", NSProcessInfo.processInfo.arguments.firstObject, getpid(), getuid()); //start sysext // Apple notes, "call [this] as early as possible" [NEProvider startSystemExtensionMode]; //dbg msg os_log_debug(logHandle, "enabled extension ('startSystemExtensionMode' was called)"); //alloc/init/load prefs preferences = [[Preferences alloc] init]; //alloc/init alerts object alerts = [[Alerts alloc] init]; //alloc/init rules object rules = [[Rules alloc] init]; //alloc/init profiles object profiles = [[Profiles alloc] init]; //alloc/init XPC comms object xpcListener = [[XPCListener alloc] init]; //dbg msg os_log_debug(logHandle, "created client XPC listener"); //need to create // create install directory? if(YES != [[NSFileManager defaultManager] fileExistsAtPath:INSTALL_DIRECTORY]) { //create it if(YES != [[NSFileManager defaultManager] createDirectoryAtPath:INSTALL_DIRECTORY withIntermediateDirectories:YES attributes:nil error:NULL]) { //err msg os_log_error(logHandle, "ERROR: failed to create install directory, %{public}@", INSTALL_DIRECTORY); //bail goto bail; } } //prep rules // first time? generate defaults rules // upgrade (v1.0)? convert to new format [rules prepare]; //load rules // if this fails, falls back to (re)generating default rules if(YES != [rules load]) { //err msg os_log_error(logHandle, "ERROR: failed to load rules from %{public}@ ...will defaulting back to defualt rules", RULES_FILE); //generate default rules if(YES != [rules generateDefaultRules]) { //err msg os_log_error(logHandle, "ERROR: failed to generate default rules"); //bail goto bail; } //save [rules save]; //(re)load rules if(YES != [rules load]) { //err msg os_log_error(logHandle, "ERROR: failed again to load rules from %{public}@ ...will exit!", RULES_FILE); //bail goto bail; } } //allow list? if(0 != preferences.preferences[PREF_USE_ALLOW_LIST]) { //dbg msg os_log_debug(logHandle, "init'ing allowing list"); //alloc/init/load allow list allowList = [[BlockOrAllowList alloc] init:preferences.preferences[PREF_ALLOW_LIST]]; } //block list? if(0 != preferences.preferences[PREF_USE_BLOCK_LIST]) { //dbg msg os_log_debug(logHandle, "init'ing block list"); //alloc/init/load block list blockList = [[BlockOrAllowList alloc] init:preferences.preferences[PREF_BLOCK_LIST]]; } }//pool dispatch_main(); bail: return 0; } ================================================ FILE: LuLu/Extension/procInfo.h ================================================ // // File: procInfo.h // Project: Proc Info // // Created by: Patrick Wardle // Copyright: 2017 Objective-See // License: Creative Commons Attribution-NonCommercial 4.0 International License // #ifndef procInfo_h #define procInfo_h #import #import #import /* CLASSES */ @class Binary; @class Process; /* DEFINES */ //from audit_kevents.h #define EVENT_EXIT 1 #define EVENT_FORK 2 #define EVENT_EXECVE 23 #define EVENT_EXEC 27 #define EVENT_SPAWN 43190 //signers enum Signer{None, Apple, AppStore, DevID, AdHoc}; //signature status #define KEY_SIGNATURE_STATUS @"signatureStatus" //signer #define KEY_SIGNATURE_SIGNER @"signatureSigner" //signing auths #define KEY_SIGNATURE_AUTHORITIES @"signatureAuthorities" //code signing id #define KEY_SIGNATURE_IDENTIFIER @"signatureIdentifier" //entitlements #define KEY_SIGNATURE_ENTITLEMENTS @"signatureEntitlements" /* TYPEDEFS */ //block for library typedef void (^ProcessCallbackBlock)(Process* _Nonnull); /* OBJECT: PROCESS INFO */ @interface ProcInfo : NSObject //init w/ flag // flag dictates if CPU-intensive logic (code signing, etc) should be preformed -(id _Nullable)init:(BOOL)goEasy; //start monitoring -(void)start:(ProcessCallbackBlock _Nonnull )callback; //stop monitoring -(void)stop; //get list of running processes -(NSMutableArray* _Nonnull)currentProcesses; @end /* OBJECT: PROCESS */ @interface Process : NSObject /* PROPERTIES */ //pid @property pid_t pid; //ppid @property pid_t ppid; //user id @property uid_t uid; //type // used by process mon @property u_int16_t type; //exit code @property u_int32_t exit; //path @property(nonatomic, retain)NSString* _Nullable path; //args @property(nonatomic, retain)NSMutableArray* _Nonnull arguments; //ancestors @property(nonatomic, retain)NSMutableArray* _Nonnull ancestors; //signing info @property(nonatomic, retain)NSMutableDictionary* _Nonnull signingInfo; //Binary object // has path, hash, etc @property(nonatomic, retain)Binary* _Nonnull binary; //timestamp @property(nonatomic, retain)NSDate* _Nonnull timestamp; /* METHODS */ //init with a pid // method will then (try) fill out rest of object -(id _Nullable)init:(pid_t)processID; //generate signing info // also classifies if Apple/from App Store/etc. -(void)generateSigningInfo:(SecCSFlags)flags; //set process's path -(void)pathFromPid; //generate list of ancestors -(void)enumerateAncestors; //class method // get's parent of arbitrary process +(pid_t)getParentID:(pid_t)child; @end /* OBJECT: BINARY */ @interface Binary : NSObject { } /* PROPERTIES */ //path @property(nonatomic, retain)NSString* _Nonnull path; //name @property(nonatomic, retain)NSString* _Nonnull name; //icon @property(nonatomic, retain)NSImage* _Nonnull icon; //file attributes @property(nonatomic, retain)NSDictionary* _Nullable attributes; //spotlight meta data @property(nonatomic, retain)NSDictionary* _Nullable metadata; //bundle // nil for non-apps @property(nonatomic, retain)NSBundle* _Nullable bundle; //signing info @property(nonatomic, retain)NSDictionary* _Nonnull signingInfo; //hash @property(nonatomic, retain)NSMutableString* _Nonnull sha256; //identifier // either signing id or sha256 hash @property(nonatomic, retain)NSString* _Nonnull identifier; /* METHODS */ //init w/ a path -(id _Nonnull)init:(NSString* _Nonnull)path; /* the following methods are rather CPU-intensive as such, if the proc monitoring is run with the 'goEasy' option, they aren't automatically invoked */ //get an icon for a process // for apps, this will be app's icon, otherwise just a standard system one -(void)getIcon; //generate signing info (statically) -(void)generateSigningInfo:(SecCSFlags)flags; /* the following methods are not invoked automatically as such, if you code has to manually invoke them if you want this info */ //generate hash // algo: sha256 -(void)generateHash; //generate id // either signing id, or sha256 hash -(void)generateIdentifier; @end #endif ================================================ FILE: LuLu/LuLu.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 60; objects = { /* Begin PBXBuildFile section */ CD0070262C3954220011979F /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = CD0070252C3954220011979F /* Localizable.xcstrings */; }; CD0070272C3954220011979F /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = CD0070252C3954220011979F /* Localizable.xcstrings */; }; CD00702C2C3956F50011979F /* Welcome.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD00702B2C3956F50011979F /* Welcome.xib */; }; CD0070302C3959970011979F /* Rules.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD00702F2C3959970011979F /* Rules.xib */; }; CD0070342C39599E0011979F /* StartupWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD0070332C39599E0011979F /* StartupWindowController.xib */; }; CD0070382C3959A90011979F /* StatusBarPopover.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD0070372C3959A90011979F /* StatusBarPopover.xib */; }; CD00703C2C3959B90011979F /* UpdateWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD00703B2C3959B90011979F /* UpdateWindow.xib */; }; CD03F8F524F8E68300723BDC /* Binary.m in Sources */ = {isa = PBXBuildFile; fileRef = CD03F8F324F8E68300723BDC /* Binary.m */; }; CD03F8F624F8E68300723BDC /* Process.m in Sources */ = {isa = PBXBuildFile; fileRef = CD03F8F424F8E68300723BDC /* Process.m */; }; CD21D37A252FE91E001A19A8 /* signing.m in Sources */ = {isa = PBXBuildFile; fileRef = CD21D379252FE91E001A19A8 /* signing.m */; }; CD21D37D252FE94F001A19A8 /* signing.m in Sources */ = {isa = PBXBuildFile; fileRef = CD21D37B252FE94F001A19A8 /* signing.m */; }; CD2CA1742C3E9E4300D7BEAA /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD2CA1732C3E9E4300D7BEAA /* Preferences.xib */; }; CD2CA1782C3E9E5700D7BEAA /* ItemPaths.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD2CA1772C3E9E5700D7BEAA /* ItemPaths.xib */; }; CD2CA17E2C3E9E6700D7BEAA /* AlertWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD2CA17D2C3E9E6700D7BEAA /* AlertWindow.xib */; }; CD2CA1822C3E9E6C00D7BEAA /* AddRule.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD2CA1812C3E9E6C00D7BEAA /* AddRule.xib */; }; CD2CA1862C3E9E7000D7BEAA /* AboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD2CA1852C3E9E7000D7BEAA /* AboutWindow.xib */; }; CD2CA1912C3E9F0A00D7BEAA /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD2CA1902C3E9F0A00D7BEAA /* MainMenu.xib */; }; CD9E04D3254163AF00DB3218 /* patrons.txt in Resources */ = {isa = PBXBuildFile; fileRef = CDA1369624F0D2CF005AD424 /* patrons.txt */; }; CD9E147D25171BF200DA05C3 /* ItemPathsWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9E147B25171BF200DA05C3 /* ItemPathsWindowController.m */; }; CD9F567F252533CE00F3DEB5 /* NSApplicationKeyEvents.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9F567E252533CD00F3DEB5 /* NSApplicationKeyEvents.m */; }; CDA1364624EF4DA1005AD424 /* utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1363324EF4DA0005AD424 /* utilities.m */; }; CDA1364824EF4DA1005AD424 /* Rule.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1363724EF4DA0005AD424 /* Rule.m */; }; CDA1366224EF4E57005AD424 /* XPCUserClient.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1364D24EF4E56005AD424 /* XPCUserClient.m */; }; CDA1366324EF4E57005AD424 /* Alerts.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1364F24EF4E56005AD424 /* Alerts.m */; }; CDA1366424EF4E57005AD424 /* GrayList.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1365124EF4E56005AD424 /* GrayList.m */; }; CDA1366624EF4E57005AD424 /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1365324EF4E56005AD424 /* Preferences.m */; }; CDA1366724EF4E57005AD424 /* XPCDaemon.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1365624EF4E56005AD424 /* XPCDaemon.m */; }; CDA1366824EF4E57005AD424 /* XPCListener.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1365724EF4E56005AD424 /* XPCListener.m */; }; CDA1366D24EF4F89005AD424 /* Rules.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1366C24EF4F89005AD424 /* Rules.m */; }; CDA1366F24EF5587005AD424 /* libbsm.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CDA1366E24EF5587005AD424 /* libbsm.tbd */; }; CDA136C724F0D7C3005AD424 /* utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA136C524F0D7C3005AD424 /* utilities.m */; }; CDA136CD24F0D94F005AD424 /* XPCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA136CC24F0D94F005AD424 /* XPCUser.m */; }; CDA136D024F0D9C1005AD424 /* XPCDaemonClient.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA136CF24F0D9C1005AD424 /* XPCDaemonClient.m */; }; CDA136D124F0DA0E005AD424 /* Update.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA136B424F0D4A8005AD424 /* Update.m */; }; CDA136D224F0DA0E005AD424 /* UpdateWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA136B324F0D4A8005AD424 /* UpdateWindowController.m */; }; CDA136D324F0DA0E005AD424 /* AboutWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1369524F0D2CF005AD424 /* AboutWindowController.m */; }; CDA136D424F0DA0E005AD424 /* AddRuleWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1369A24F0D2CF005AD424 /* AddRuleWindowController.m */; }; CDA136D524F0DA0E005AD424 /* PrefsWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1368E24F0D2CF005AD424 /* PrefsWindowController.m */; }; CDA136D624F0DA0E005AD424 /* RuleRow.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1369E24F0D2CF005AD424 /* RuleRow.m */; }; CDA136D924F0DA0E005AD424 /* RulesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1369C24F0D2CF005AD424 /* RulesWindowController.m */; }; CDA136DA24F0DA0E005AD424 /* WelcomeWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1369924F0D2CF005AD424 /* WelcomeWindowController.m */; }; CDA136DB24F0DA0E005AD424 /* AlertWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1367224F0D268005AD424 /* AlertWindowController.m */; }; CDA136DD24F0DA0F005AD424 /* ParentsWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1367E24F0D268005AD424 /* ParentsWindowController.m */; }; CDA136DE24F0DA0F005AD424 /* SigningInfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1367524F0D268005AD424 /* SigningInfoViewController.m */; }; CDA136DF24F0DA0F005AD424 /* StatusBarItem.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1368024F0D268005AD424 /* StatusBarItem.m */; }; CDA136E024F0DA0F005AD424 /* StatusBarPopoverController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1367C24F0D268005AD424 /* StatusBarPopoverController.m */; }; CDA136E524F0DA43005AD424 /* Rule.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA136E424F0DA43005AD424 /* Rule.m */; }; CDB2CC1D24D61A4E00D0EECE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB2CC1C24D61A4E00D0EECE /* AppDelegate.m */; }; CDB2CC1F24D61A5000D0EECE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CDB2CC1E24D61A5000D0EECE /* Assets.xcassets */; }; CDB2CC2524D61A5000D0EECE /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB2CC2424D61A5000D0EECE /* main.m */; }; CDB2CC3324D61B3900D0EECE /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDB2CC3224D61B3900D0EECE /* NetworkExtension.framework */; }; CDB2CC3724D61B3900D0EECE /* FilterDataProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB2CC3624D61B3900D0EECE /* FilterDataProvider.m */; }; CDB2CC3924D61B3900D0EECE /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB2CC3824D61B3900D0EECE /* main.m */; }; CDB2CC3E24D61B3900D0EECE /* com.objective-see.lulu.extension.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = CDB2CC3024D61B3900D0EECE /* com.objective-see.lulu.extension.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; CDB2CC4424DBE48100D0EECE /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDB2CC3224D61B3900D0EECE /* NetworkExtension.framework */; }; CDB909EB2B72BC230043FEB4 /* Configure.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB909EA2B72BC230043FEB4 /* Configure.m */; }; CDC378C7250C66C300314064 /* Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC378C6250C66C300314064 /* Extension.m */; }; CDC378CB250C83F200314064 /* Netiquette.app in Resources */ = {isa = PBXBuildFile; fileRef = CDC378C9250C83F100314064 /* Netiquette.app */; }; CDC41C412503424800CB302B /* OrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC41C3F2503424800CB302B /* OrderedDictionary.m */; }; CDCAA9692B677AFC00FE27DD /* StartupWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDCAA9662B677AFC00FE27DD /* StartupWindowController.m */; }; CDCAA96C2B69DC3700FE27DD /* RulesMenuController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDCAA96B2B69DC3700FE27DD /* RulesMenuController.m */; }; CDD7853D255609AC001BB0BE /* BlockOrAllowList.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD7853C255609AC001BB0BE /* BlockOrAllowList.m */; }; CDD992D72C4EC30000A1B406 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = CDD992D62C4EC30000A1B406 /* InfoPlist.xcstrings */; }; CDEA3AD22E0724EC00FDD0C0 /* Profiles.m in Sources */ = {isa = PBXBuildFile; fileRef = CDEA3AD12E0724EC00FDD0C0 /* Profiles.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ CDB2CC3C24D61B3900D0EECE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CDB2CC1024D61A4E00D0EECE /* Project object */; proxyType = 1; remoteGlobalIDString = CDB2CC2F24D61B3900D0EECE; remoteInfo = Extension; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ CDB2CC4224D61B3900D0EECE /* Embed System Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 12; dstPath = "$(SYSTEM_EXTENSIONS_FOLDER_PATH)"; dstSubfolderSpec = 16; files = ( CDB2CC3E24D61B3900D0EECE /* com.objective-see.lulu.extension.systemextension in Embed System Extensions */, ); name = "Embed System Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ CD0070252C3954220011979F /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Shared/Localizable.xcstrings; sourceTree = ""; }; CD00702A2C3956F50011979F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Welcome.xib; sourceTree = ""; }; CD00702D2C3956F50011979F /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Welcome.xcstrings; sourceTree = ""; }; CD00702E2C3959970011979F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Rules.xib; sourceTree = ""; }; CD0070312C3959970011979F /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Rules.xcstrings; sourceTree = ""; }; CD0070322C39599E0011979F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/StartupWindowController.xib; sourceTree = ""; }; CD0070352C39599E0011979F /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/StartupWindowController.xcstrings; sourceTree = ""; }; CD0070362C3959A90011979F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/StatusBarPopover.xib; sourceTree = ""; }; CD0070392C3959A90011979F /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/StatusBarPopover.xcstrings; sourceTree = ""; }; CD00703A2C3959B90011979F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UpdateWindow.xib; sourceTree = ""; }; CD00703D2C3959B90011979F /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/UpdateWindow.xcstrings; sourceTree = ""; }; CD03F8F324F8E68300723BDC /* Binary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Binary.m; sourceTree = ""; }; CD03F8F424F8E68300723BDC /* Process.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Process.m; sourceTree = ""; }; CD03F8FA24F8E6BD00723BDC /* Binary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Binary.h; sourceTree = ""; }; CD03F8FB24F8E6C600723BDC /* Process.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Process.h; sourceTree = ""; }; CD21D378252FE91E001A19A8 /* signing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = signing.h; path = Shared/signing.h; sourceTree = SOURCE_ROOT; }; CD21D379252FE91E001A19A8 /* signing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = signing.m; path = Shared/signing.m; sourceTree = SOURCE_ROOT; }; CD21D37B252FE94F001A19A8 /* signing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = signing.m; path = Shared/signing.m; sourceTree = SOURCE_ROOT; }; CD21D37C252FE94F001A19A8 /* signing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = signing.h; path = Shared/signing.h; sourceTree = SOURCE_ROOT; }; CD2CA1722C3E9E4300D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Preferences.xib; sourceTree = ""; }; CD2CA1752C3E9E4300D7BEAA /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Preferences.xcstrings; sourceTree = ""; }; CD2CA1762C3E9E5700D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ItemPaths.xib; sourceTree = ""; }; CD2CA1792C3E9E5700D7BEAA /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ItemPaths.xcstrings; sourceTree = ""; }; CD2CA17C2C3E9E6700D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AlertWindow.xib; sourceTree = ""; }; CD2CA17F2C3E9E6700D7BEAA /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/AlertWindow.xcstrings; sourceTree = ""; }; CD2CA1802C3E9E6C00D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AddRule.xib; sourceTree = ""; }; CD2CA1832C3E9E6C00D7BEAA /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/AddRule.xcstrings; sourceTree = ""; }; CD2CA1842C3E9E7000D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AboutWindow.xib; sourceTree = ""; }; CD2CA1872C3E9E7000D7BEAA /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/AboutWindow.xcstrings; sourceTree = ""; }; CD2CA18A2C3E9ED700D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Base; path = Base.lproj/Extension.entitlements; sourceTree = ""; }; CD2CA18C2C3E9EF200D7BEAA /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; CD2CA18F2C3E9F0A00D7BEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; CD2CA1922C3E9F0A00D7BEAA /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/MainMenu.xcstrings; sourceTree = ""; }; CD9E147A25171BF200DA05C3 /* ItemPathsWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ItemPathsWindowController.h; sourceTree = ""; }; CD9E147B25171BF200DA05C3 /* ItemPathsWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ItemPathsWindowController.m; sourceTree = ""; }; CD9F567D252533CD00F3DEB5 /* NSApplicationKeyEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSApplicationKeyEvents.h; sourceTree = ""; }; CD9F567E252533CD00F3DEB5 /* NSApplicationKeyEvents.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSApplicationKeyEvents.m; sourceTree = ""; }; CDA1363224EF4DA0005AD424 /* utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = utilities.h; path = Shared/utilities.h; sourceTree = SOURCE_ROOT; }; CDA1363324EF4DA0005AD424 /* utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = utilities.m; path = Shared/utilities.m; sourceTree = SOURCE_ROOT; }; CDA1363424EF4DA0005AD424 /* XPCDaemonProto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = XPCDaemonProto.h; path = Shared/XPCDaemonProto.h; sourceTree = SOURCE_ROOT; }; CDA1363724EF4DA0005AD424 /* Rule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Rule.m; path = Shared/Rule.m; sourceTree = SOURCE_ROOT; }; CDA1363824EF4DA0005AD424 /* Rule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Rule.h; path = Shared/Rule.h; sourceTree = SOURCE_ROOT; }; CDA1363A24EF4DA0005AD424 /* consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = consts.h; path = Shared/consts.h; sourceTree = SOURCE_ROOT; }; CDA1364124EF4DA1005AD424 /* XPCUserProto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = XPCUserProto.h; path = Shared/XPCUserProto.h; sourceTree = SOURCE_ROOT; }; CDA1364D24EF4E56005AD424 /* XPCUserClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCUserClient.m; sourceTree = ""; }; CDA1364F24EF4E56005AD424 /* Alerts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Alerts.m; sourceTree = ""; }; CDA1365024EF4E56005AD424 /* main.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = main.h; sourceTree = ""; }; CDA1365124EF4E56005AD424 /* GrayList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GrayList.m; sourceTree = ""; }; CDA1365324EF4E56005AD424 /* Preferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = ""; }; CDA1365624EF4E56005AD424 /* XPCDaemon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCDaemon.m; sourceTree = ""; }; CDA1365724EF4E56005AD424 /* XPCListener.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCListener.m; sourceTree = ""; }; CDA1365924EF4E56005AD424 /* XPCUserClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XPCUserClient.h; sourceTree = ""; }; CDA1365A24EF4E56005AD424 /* Alerts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Alerts.h; sourceTree = ""; }; CDA1365B24EF4E57005AD424 /* Preferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = ""; }; CDA1365C24EF4E57005AD424 /* XPCListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XPCListener.h; sourceTree = ""; }; CDA1365D24EF4E57005AD424 /* GrayList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GrayList.h; sourceTree = ""; }; CDA1365E24EF4E57005AD424 /* XPCDaemon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XPCDaemon.h; sourceTree = ""; }; CDA1366B24EF4F89005AD424 /* Rules.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Rules.h; sourceTree = ""; }; CDA1366C24EF4F89005AD424 /* Rules.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Rules.m; sourceTree = ""; }; CDA1366E24EF5587005AD424 /* libbsm.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbsm.tbd; path = usr/lib/libbsm.tbd; sourceTree = SDKROOT; }; CDA1367224F0D268005AD424 /* AlertWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AlertWindowController.m; sourceTree = ""; }; CDA1367524F0D268005AD424 /* SigningInfoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SigningInfoViewController.m; sourceTree = ""; }; CDA1367724F0D268005AD424 /* StatusBarPopoverController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StatusBarPopoverController.h; sourceTree = ""; }; CDA1367824F0D268005AD424 /* SigningInfoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SigningInfoViewController.h; sourceTree = ""; }; CDA1367A24F0D268005AD424 /* ParentsWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParentsWindowController.h; sourceTree = ""; }; CDA1367C24F0D268005AD424 /* StatusBarPopoverController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StatusBarPopoverController.m; sourceTree = ""; }; CDA1367E24F0D268005AD424 /* ParentsWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ParentsWindowController.m; sourceTree = ""; }; CDA1367F24F0D268005AD424 /* AlertWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AlertWindowController.h; sourceTree = ""; }; CDA1368024F0D268005AD424 /* StatusBarItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StatusBarItem.m; sourceTree = ""; }; CDA1368224F0D269005AD424 /* StatusBarItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StatusBarItem.h; sourceTree = ""; }; CDA1368E24F0D2CF005AD424 /* PrefsWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrefsWindowController.m; sourceTree = ""; }; CDA1368F24F0D2CF005AD424 /* RulesWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RulesWindowController.h; sourceTree = ""; }; CDA1369224F0D2CF005AD424 /* PrefsWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrefsWindowController.h; sourceTree = ""; }; CDA1369324F0D2CF005AD424 /* AddRuleWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddRuleWindowController.h; sourceTree = ""; }; CDA1369524F0D2CF005AD424 /* AboutWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AboutWindowController.m; sourceTree = ""; }; CDA1369624F0D2CF005AD424 /* patrons.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = patrons.txt; sourceTree = ""; }; CDA1369724F0D2CF005AD424 /* WelcomeWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WelcomeWindowController.h; sourceTree = ""; }; CDA1369924F0D2CF005AD424 /* WelcomeWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WelcomeWindowController.m; sourceTree = ""; }; CDA1369A24F0D2CF005AD424 /* AddRuleWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddRuleWindowController.m; sourceTree = ""; }; CDA1369B24F0D2CF005AD424 /* AboutWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AboutWindowController.h; sourceTree = ""; }; CDA1369C24F0D2CF005AD424 /* RulesWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RulesWindowController.m; sourceTree = ""; }; CDA1369D24F0D2CF005AD424 /* RuleRow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RuleRow.h; sourceTree = ""; }; CDA1369E24F0D2CF005AD424 /* RuleRow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RuleRow.m; sourceTree = ""; }; CDA136B324F0D4A8005AD424 /* UpdateWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UpdateWindowController.m; sourceTree = ""; }; CDA136B424F0D4A8005AD424 /* Update.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Update.m; sourceTree = ""; }; CDA136B524F0D4A9005AD424 /* Update.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Update.h; sourceTree = ""; }; CDA136B624F0D4A9005AD424 /* UpdateWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UpdateWindowController.h; sourceTree = ""; }; CDA136C424F0D7C3005AD424 /* utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = utilities.h; path = Shared/utilities.h; sourceTree = SOURCE_ROOT; }; CDA136C524F0D7C3005AD424 /* utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = utilities.m; path = Shared/utilities.m; sourceTree = SOURCE_ROOT; }; CDA136C624F0D7C3005AD424 /* consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = consts.h; path = Shared/consts.h; sourceTree = SOURCE_ROOT; }; CDA136CB24F0D94F005AD424 /* XPCUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XPCUser.h; sourceTree = ""; }; CDA136CC24F0D94F005AD424 /* XPCUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCUser.m; sourceTree = ""; }; CDA136CE24F0D9C1005AD424 /* XPCDaemonClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XPCDaemonClient.h; sourceTree = ""; }; CDA136CF24F0D9C1005AD424 /* XPCDaemonClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCDaemonClient.m; sourceTree = ""; }; CDA136E324F0DA43005AD424 /* Rule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Rule.h; path = Shared/Rule.h; sourceTree = SOURCE_ROOT; }; CDA136E424F0DA43005AD424 /* Rule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Rule.m; path = Shared/Rule.m; sourceTree = SOURCE_ROOT; }; CDB2CC1824D61A4E00D0EECE /* LuLu.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LuLu.app; sourceTree = BUILT_PRODUCTS_DIR; }; CDB2CC1B24D61A4E00D0EECE /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; CDB2CC1C24D61A4E00D0EECE /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; CDB2CC1E24D61A5000D0EECE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CDB2CC2324D61A5000D0EECE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CDB2CC2424D61A5000D0EECE /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; CDB2CC3024D61B3900D0EECE /* com.objective-see.lulu.extension.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = "com.objective-see.lulu.extension.systemextension"; sourceTree = BUILT_PRODUCTS_DIR; }; CDB2CC3224D61B3900D0EECE /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; CDB2CC3524D61B3900D0EECE /* FilterDataProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FilterDataProvider.h; sourceTree = ""; }; CDB2CC3624D61B3900D0EECE /* FilterDataProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FilterDataProvider.m; sourceTree = ""; }; CDB2CC3824D61B3900D0EECE /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; CDB2CC3A24D61B3900D0EECE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CDB909EA2B72BC230043FEB4 /* Configure.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Configure.m; sourceTree = ""; }; CDB909EC2B72BC2E0043FEB4 /* Configure.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Configure.h; sourceTree = ""; }; CDC378C5250C66C200314064 /* Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Extension.h; sourceTree = ""; }; CDC378C6250C66C300314064 /* Extension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Extension.m; sourceTree = ""; }; CDC378C9250C83F100314064 /* Netiquette.app */ = {isa = PBXFileReference; lastKnownFileType = wrapper.application; path = Netiquette.app; sourceTree = ""; }; CDC41C3F2503424800CB302B /* OrderedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OrderedDictionary.m; path = "3rd-party/OrderedDictionary.m"; sourceTree = ""; }; CDC41C402503424800CB302B /* OrderedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OrderedDictionary.h; path = "3rd-party/OrderedDictionary.h"; sourceTree = ""; }; CDCAA9662B677AFC00FE27DD /* StartupWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StartupWindowController.m; sourceTree = ""; }; CDCAA9672B677AFC00FE27DD /* StartupWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StartupWindowController.h; sourceTree = ""; }; CDCAA96B2B69DC3700FE27DD /* RulesMenuController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RulesMenuController.m; sourceTree = ""; }; CDCAA96D2B69DC4500FE27DD /* RulesMenuController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RulesMenuController.h; sourceTree = ""; }; CDD7853B255609AC001BB0BE /* BlockOrAllowList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlockOrAllowList.h; sourceTree = ""; }; CDD7853C255609AC001BB0BE /* BlockOrAllowList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BlockOrAllowList.m; sourceTree = ""; }; CDD992D62C4EC30000A1B406 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; CDEA3AD02E0724EC00FDD0C0 /* Profiles.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Profiles.h; sourceTree = ""; }; CDEA3AD12E0724EC00FDD0C0 /* Profiles.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Profiles.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ CDB2CC1524D61A4E00D0EECE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CDB2CC4424DBE48100D0EECE /* NetworkExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; CDB2CC2D24D61B3900D0EECE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CDA1366F24EF5587005AD424 /* libbsm.tbd in Frameworks */, CDB2CC3324D61B3900D0EECE /* NetworkExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ CDA135F824EBB58E005AD424 /* Shared */ = { isa = PBXGroup; children = ( CDA1363A24EF4DA0005AD424 /* consts.h */, CDA1363824EF4DA0005AD424 /* Rule.h */, CDA1363724EF4DA0005AD424 /* Rule.m */, CD21D378252FE91E001A19A8 /* signing.h */, CD21D379252FE91E001A19A8 /* signing.m */, CDA1363224EF4DA0005AD424 /* utilities.h */, CDA1363324EF4DA0005AD424 /* utilities.m */, CDA1363424EF4DA0005AD424 /* XPCDaemonProto.h */, CDA1364124EF4DA1005AD424 /* XPCUserProto.h */, ); name = Shared; sourceTree = ""; }; CDA136BD24F0D526005AD424 /* Shared */ = { isa = PBXGroup; children = ( CDA136C624F0D7C3005AD424 /* consts.h */, CDA136E324F0DA43005AD424 /* Rule.h */, CDA136E424F0DA43005AD424 /* Rule.m */, CD21D37C252FE94F001A19A8 /* signing.h */, CD21D37B252FE94F001A19A8 /* signing.m */, CDA136C424F0D7C3005AD424 /* utilities.h */, CDA136C524F0D7C3005AD424 /* utilities.m */, ); name = Shared; sourceTree = ""; }; CDB2CC0F24D61A4E00D0EECE = { isa = PBXGroup; children = ( CD0070252C3954220011979F /* Localizable.xcstrings */, CDB2CC1A24D61A4E00D0EECE /* App */, CDB2CC3424D61B3900D0EECE /* Extension */, CDB2CC3124D61B3900D0EECE /* Frameworks */, CDB2CC1924D61A4E00D0EECE /* Products */, ); sourceTree = ""; }; CDB2CC1924D61A4E00D0EECE /* Products */ = { isa = PBXGroup; children = ( CDB2CC1824D61A4E00D0EECE /* LuLu.app */, CDB2CC3024D61B3900D0EECE /* com.objective-see.lulu.extension.systemextension */, ); name = Products; sourceTree = ""; }; CDB2CC1A24D61A4E00D0EECE /* App */ = { isa = PBXGroup; children = ( CD2CA1852C3E9E7000D7BEAA /* AboutWindow.xib */, CDA1369B24F0D2CF005AD424 /* AboutWindowController.h */, CDA1369524F0D2CF005AD424 /* AboutWindowController.m */, CD2CA1812C3E9E6C00D7BEAA /* AddRule.xib */, CDA1369324F0D2CF005AD424 /* AddRuleWindowController.h */, CDA1369A24F0D2CF005AD424 /* AddRuleWindowController.m */, CD2CA17D2C3E9E6700D7BEAA /* AlertWindow.xib */, CDA1367F24F0D268005AD424 /* AlertWindowController.h */, CDA1367224F0D268005AD424 /* AlertWindowController.m */, CD2CA18C2C3E9EF200D7BEAA /* App.entitlements */, CDB2CC1B24D61A4E00D0EECE /* AppDelegate.h */, CDB2CC1C24D61A4E00D0EECE /* AppDelegate.m */, CDB2CC1E24D61A5000D0EECE /* Assets.xcassets */, CDC378C8250C83F100314064 /* Binaries */, CDB909EC2B72BC2E0043FEB4 /* Configure.h */, CDB909EA2B72BC230043FEB4 /* Configure.m */, CDC378C5250C66C200314064 /* Extension.h */, CDC378C6250C66C300314064 /* Extension.m */, CDB2CC2324D61A5000D0EECE /* Info.plist */, CDD992D62C4EC30000A1B406 /* InfoPlist.xcstrings */, CD2CA1772C3E9E5700D7BEAA /* ItemPaths.xib */, CD9E147A25171BF200DA05C3 /* ItemPathsWindowController.h */, CD9E147B25171BF200DA05C3 /* ItemPathsWindowController.m */, CDB2CC2424D61A5000D0EECE /* main.m */, CD2CA1902C3E9F0A00D7BEAA /* MainMenu.xib */, CD9F567D252533CD00F3DEB5 /* NSApplicationKeyEvents.h */, CD9F567E252533CD00F3DEB5 /* NSApplicationKeyEvents.m */, CDC41C402503424800CB302B /* OrderedDictionary.h */, CDC41C3F2503424800CB302B /* OrderedDictionary.m */, CDA1367A24F0D268005AD424 /* ParentsWindowController.h */, CDA1367E24F0D268005AD424 /* ParentsWindowController.m */, CDA1369624F0D2CF005AD424 /* patrons.txt */, CD2CA1732C3E9E4300D7BEAA /* Preferences.xib */, CDA1369224F0D2CF005AD424 /* PrefsWindowController.h */, CDA1368E24F0D2CF005AD424 /* PrefsWindowController.m */, CDA1369D24F0D2CF005AD424 /* RuleRow.h */, CDA1369E24F0D2CF005AD424 /* RuleRow.m */, CD00702F2C3959970011979F /* Rules.xib */, CDCAA96D2B69DC4500FE27DD /* RulesMenuController.h */, CDCAA96B2B69DC3700FE27DD /* RulesMenuController.m */, CDA1368F24F0D2CF005AD424 /* RulesWindowController.h */, CDA1369C24F0D2CF005AD424 /* RulesWindowController.m */, CDA136BD24F0D526005AD424 /* Shared */, CDA1367824F0D268005AD424 /* SigningInfoViewController.h */, CDA1367524F0D268005AD424 /* SigningInfoViewController.m */, CDCAA9672B677AFC00FE27DD /* StartupWindowController.h */, CDCAA9662B677AFC00FE27DD /* StartupWindowController.m */, CD0070332C39599E0011979F /* StartupWindowController.xib */, CDA1368224F0D269005AD424 /* StatusBarItem.h */, CDA1368024F0D268005AD424 /* StatusBarItem.m */, CD0070372C3959A90011979F /* StatusBarPopover.xib */, CDA1367724F0D268005AD424 /* StatusBarPopoverController.h */, CDA1367C24F0D268005AD424 /* StatusBarPopoverController.m */, CDA136B524F0D4A9005AD424 /* Update.h */, CDA136B424F0D4A8005AD424 /* Update.m */, CD00703B2C3959B90011979F /* UpdateWindow.xib */, CDA136B624F0D4A9005AD424 /* UpdateWindowController.h */, CDA136B324F0D4A8005AD424 /* UpdateWindowController.m */, CD00702B2C3956F50011979F /* Welcome.xib */, CDA1369724F0D2CF005AD424 /* WelcomeWindowController.h */, CDA1369924F0D2CF005AD424 /* WelcomeWindowController.m */, CDA136CE24F0D9C1005AD424 /* XPCDaemonClient.h */, CDA136CF24F0D9C1005AD424 /* XPCDaemonClient.m */, CDA136CB24F0D94F005AD424 /* XPCUser.h */, CDA136CC24F0D94F005AD424 /* XPCUser.m */, ); path = App; sourceTree = ""; }; CDB2CC3124D61B3900D0EECE /* Frameworks */ = { isa = PBXGroup; children = ( CDA1366E24EF5587005AD424 /* libbsm.tbd */, CDB2CC3224D61B3900D0EECE /* NetworkExtension.framework */, ); name = Frameworks; sourceTree = ""; }; CDB2CC3424D61B3900D0EECE /* Extension */ = { isa = PBXGroup; children = ( CDA1365A24EF4E56005AD424 /* Alerts.h */, CDD7853B255609AC001BB0BE /* BlockOrAllowList.h */, CDD7853C255609AC001BB0BE /* BlockOrAllowList.m */, CDA1364F24EF4E56005AD424 /* Alerts.m */, CD03F8FA24F8E6BD00723BDC /* Binary.h */, CD03F8F324F8E68300723BDC /* Binary.m */, CD2CA18B2C3E9ED700D7BEAA /* Extension.entitlements */, CDB2CC3524D61B3900D0EECE /* FilterDataProvider.h */, CDB2CC3624D61B3900D0EECE /* FilterDataProvider.m */, CDA1365D24EF4E57005AD424 /* GrayList.h */, CDA1365124EF4E56005AD424 /* GrayList.m */, CDB2CC3A24D61B3900D0EECE /* Info.plist */, CDA1365024EF4E56005AD424 /* main.h */, CDB2CC3824D61B3900D0EECE /* main.m */, CDA1365B24EF4E57005AD424 /* Preferences.h */, CDA1365324EF4E56005AD424 /* Preferences.m */, CDEA3AD02E0724EC00FDD0C0 /* Profiles.h */, CDEA3AD12E0724EC00FDD0C0 /* Profiles.m */, CD03F8FB24F8E6C600723BDC /* Process.h */, CD03F8F424F8E68300723BDC /* Process.m */, CDA1366B24EF4F89005AD424 /* Rules.h */, CDA1366C24EF4F89005AD424 /* Rules.m */, CDA135F824EBB58E005AD424 /* Shared */, CDA1365E24EF4E57005AD424 /* XPCDaemon.h */, CDA1365624EF4E56005AD424 /* XPCDaemon.m */, CDA1365C24EF4E57005AD424 /* XPCListener.h */, CDA1365724EF4E56005AD424 /* XPCListener.m */, CDA1365924EF4E56005AD424 /* XPCUserClient.h */, CDA1364D24EF4E56005AD424 /* XPCUserClient.m */, ); path = Extension; sourceTree = ""; }; CDC378C8250C83F100314064 /* Binaries */ = { isa = PBXGroup; children = ( CDC378C9250C83F100314064 /* Netiquette.app */, ); path = Binaries; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ CDB2CC1724D61A4E00D0EECE /* LuLu */ = { isa = PBXNativeTarget; buildConfigurationList = CDB2CC2924D61A5000D0EECE /* Build configuration list for PBXNativeTarget "LuLu" */; buildPhases = ( CDB2CC1424D61A4E00D0EECE /* Sources */, CDB2CC1524D61A4E00D0EECE /* Frameworks */, CDB2CC1624D61A4E00D0EECE /* Resources */, CDB2CC4224D61B3900D0EECE /* Embed System Extensions */, ); buildRules = ( ); dependencies = ( CDB2CC3D24D61B3900D0EECE /* PBXTargetDependency */, ); name = LuLu; productName = TestExtension; productReference = CDB2CC1824D61A4E00D0EECE /* LuLu.app */; productType = "com.apple.product-type.application"; }; CDB2CC2F24D61B3900D0EECE /* Extension */ = { isa = PBXNativeTarget; buildConfigurationList = CDB2CC3F24D61B3900D0EECE /* Build configuration list for PBXNativeTarget "Extension" */; buildPhases = ( CDB2CC2C24D61B3900D0EECE /* Sources */, CDB2CC2D24D61B3900D0EECE /* Frameworks */, CDB2CC2E24D61B3900D0EECE /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Extension; productName = Extension; productReference = CDB2CC3024D61B3900D0EECE /* com.objective-see.lulu.extension.systemextension */; productType = "com.apple.product-type.system-extension"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ CDB2CC1024D61A4E00D0EECE /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1160; ORGANIZATIONNAME = "Objective-See"; TargetAttributes = { CDB2CC1724D61A4E00D0EECE = { CreatedOnToolsVersion = 11.6; }; CDB2CC2F24D61B3900D0EECE = { CreatedOnToolsVersion = 11.6; }; }; }; buildConfigurationList = CDB2CC1324D61A4E00D0EECE /* Build configuration list for PBXProject "LuLu" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, es, de, it, fr, tr, "pt-BR", pl, ko, "zh-Hant", uk, "zh-Hans", ur, ); mainGroup = CDB2CC0F24D61A4E00D0EECE; productRefGroup = CDB2CC1924D61A4E00D0EECE /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( CDB2CC1724D61A4E00D0EECE /* LuLu */, CDB2CC2F24D61B3900D0EECE /* Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ CDB2CC1624D61A4E00D0EECE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( CD9E04D3254163AF00DB3218 /* patrons.txt in Resources */, CD0070342C39599E0011979F /* StartupWindowController.xib in Resources */, CDC378CB250C83F200314064 /* Netiquette.app in Resources */, CD2CA1782C3E9E5700D7BEAA /* ItemPaths.xib in Resources */, CD2CA1862C3E9E7000D7BEAA /* AboutWindow.xib in Resources */, CD00703C2C3959B90011979F /* UpdateWindow.xib in Resources */, CD2CA1822C3E9E6C00D7BEAA /* AddRule.xib in Resources */, CD2CA1742C3E9E4300D7BEAA /* Preferences.xib in Resources */, CD0070302C3959970011979F /* Rules.xib in Resources */, CD00702C2C3956F50011979F /* Welcome.xib in Resources */, CD2CA17E2C3E9E6700D7BEAA /* AlertWindow.xib in Resources */, CD0070262C3954220011979F /* Localizable.xcstrings in Resources */, CD0070382C3959A90011979F /* StatusBarPopover.xib in Resources */, CDB2CC1F24D61A5000D0EECE /* Assets.xcassets in Resources */, CDD992D72C4EC30000A1B406 /* InfoPlist.xcstrings in Resources */, CD2CA1912C3E9F0A00D7BEAA /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; CDB2CC2E24D61B3900D0EECE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( CD0070272C3954220011979F /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ CDB2CC1424D61A4E00D0EECE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CDA136E524F0DA43005AD424 /* Rule.m in Sources */, CDA136D124F0DA0E005AD424 /* Update.m in Sources */, CDA136D224F0DA0E005AD424 /* UpdateWindowController.m in Sources */, CDA136D324F0DA0E005AD424 /* AboutWindowController.m in Sources */, CDA136D424F0DA0E005AD424 /* AddRuleWindowController.m in Sources */, CDA136D524F0DA0E005AD424 /* PrefsWindowController.m in Sources */, CDA136D624F0DA0E005AD424 /* RuleRow.m in Sources */, CDCAA9692B677AFC00FE27DD /* StartupWindowController.m in Sources */, CDA136D924F0DA0E005AD424 /* RulesWindowController.m in Sources */, CDA136DA24F0DA0E005AD424 /* WelcomeWindowController.m in Sources */, CDA136DB24F0DA0E005AD424 /* AlertWindowController.m in Sources */, CDA136DD24F0DA0F005AD424 /* ParentsWindowController.m in Sources */, CDA136DE24F0DA0F005AD424 /* SigningInfoViewController.m in Sources */, CDA136DF24F0DA0F005AD424 /* StatusBarItem.m in Sources */, CDA136E024F0DA0F005AD424 /* StatusBarPopoverController.m in Sources */, CDCAA96C2B69DC3700FE27DD /* RulesMenuController.m in Sources */, CDA136D024F0D9C1005AD424 /* XPCDaemonClient.m in Sources */, CD21D37D252FE94F001A19A8 /* signing.m in Sources */, CDA136C724F0D7C3005AD424 /* utilities.m in Sources */, CDB2CC2524D61A5000D0EECE /* main.m in Sources */, CD9E147D25171BF200DA05C3 /* ItemPathsWindowController.m in Sources */, CDB909EB2B72BC230043FEB4 /* Configure.m in Sources */, CDB2CC1D24D61A4E00D0EECE /* AppDelegate.m in Sources */, CD9F567F252533CE00F3DEB5 /* NSApplicationKeyEvents.m in Sources */, CDC41C412503424800CB302B /* OrderedDictionary.m in Sources */, CDC378C7250C66C300314064 /* Extension.m in Sources */, CDA136CD24F0D94F005AD424 /* XPCUser.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CDB2CC2C24D61B3900D0EECE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CDA1364624EF4DA1005AD424 /* utilities.m in Sources */, CD21D37A252FE91E001A19A8 /* signing.m in Sources */, CDA1366824EF4E57005AD424 /* XPCListener.m in Sources */, CDA1364824EF4DA1005AD424 /* Rule.m in Sources */, CD03F8F624F8E68300723BDC /* Process.m in Sources */, CDA1366424EF4E57005AD424 /* GrayList.m in Sources */, CDB2CC3924D61B3900D0EECE /* main.m in Sources */, CDA1366724EF4E57005AD424 /* XPCDaemon.m in Sources */, CDA1366324EF4E57005AD424 /* Alerts.m in Sources */, CDEA3AD22E0724EC00FDD0C0 /* Profiles.m in Sources */, CDD7853D255609AC001BB0BE /* BlockOrAllowList.m in Sources */, CDB2CC3724D61B3900D0EECE /* FilterDataProvider.m in Sources */, CDA1366224EF4E57005AD424 /* XPCUserClient.m in Sources */, CDA1366D24EF4F89005AD424 /* Rules.m in Sources */, CDA1366624EF4E57005AD424 /* Preferences.m in Sources */, CD03F8F524F8E68300723BDC /* Binary.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ CDB2CC3D24D61B3900D0EECE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CDB2CC2F24D61B3900D0EECE /* Extension */; targetProxy = CDB2CC3C24D61B3900D0EECE /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ CD00702B2C3956F50011979F /* Welcome.xib */ = { isa = PBXVariantGroup; children = ( CD00702A2C3956F50011979F /* Base */, CD00702D2C3956F50011979F /* mul */, ); name = Welcome.xib; sourceTree = ""; }; CD00702F2C3959970011979F /* Rules.xib */ = { isa = PBXVariantGroup; children = ( CD00702E2C3959970011979F /* Base */, CD0070312C3959970011979F /* mul */, ); name = Rules.xib; sourceTree = ""; }; CD0070332C39599E0011979F /* StartupWindowController.xib */ = { isa = PBXVariantGroup; children = ( CD0070322C39599E0011979F /* Base */, CD0070352C39599E0011979F /* mul */, ); name = StartupWindowController.xib; sourceTree = ""; }; CD0070372C3959A90011979F /* StatusBarPopover.xib */ = { isa = PBXVariantGroup; children = ( CD0070362C3959A90011979F /* Base */, CD0070392C3959A90011979F /* mul */, ); name = StatusBarPopover.xib; sourceTree = ""; }; CD00703B2C3959B90011979F /* UpdateWindow.xib */ = { isa = PBXVariantGroup; children = ( CD00703A2C3959B90011979F /* Base */, CD00703D2C3959B90011979F /* mul */, ); name = UpdateWindow.xib; sourceTree = ""; }; CD2CA1732C3E9E4300D7BEAA /* Preferences.xib */ = { isa = PBXVariantGroup; children = ( CD2CA1722C3E9E4300D7BEAA /* Base */, CD2CA1752C3E9E4300D7BEAA /* mul */, ); name = Preferences.xib; sourceTree = ""; }; CD2CA1772C3E9E5700D7BEAA /* ItemPaths.xib */ = { isa = PBXVariantGroup; children = ( CD2CA1762C3E9E5700D7BEAA /* Base */, CD2CA1792C3E9E5700D7BEAA /* mul */, ); name = ItemPaths.xib; sourceTree = ""; }; CD2CA17D2C3E9E6700D7BEAA /* AlertWindow.xib */ = { isa = PBXVariantGroup; children = ( CD2CA17C2C3E9E6700D7BEAA /* Base */, CD2CA17F2C3E9E6700D7BEAA /* mul */, ); name = AlertWindow.xib; sourceTree = ""; }; CD2CA1812C3E9E6C00D7BEAA /* AddRule.xib */ = { isa = PBXVariantGroup; children = ( CD2CA1802C3E9E6C00D7BEAA /* Base */, CD2CA1832C3E9E6C00D7BEAA /* mul */, ); name = AddRule.xib; sourceTree = ""; }; CD2CA1852C3E9E7000D7BEAA /* AboutWindow.xib */ = { isa = PBXVariantGroup; children = ( CD2CA1842C3E9E7000D7BEAA /* Base */, CD2CA1872C3E9E7000D7BEAA /* mul */, ); name = AboutWindow.xib; sourceTree = ""; }; CD2CA18B2C3E9ED700D7BEAA /* Extension.entitlements */ = { isa = PBXVariantGroup; children = ( CD2CA18A2C3E9ED700D7BEAA /* Base */, ); name = Extension.entitlements; sourceTree = ""; }; CD2CA1902C3E9F0A00D7BEAA /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( CD2CA18F2C3E9F0A00D7BEAA /* Base */, CD2CA1922C3E9F0A00D7BEAA /* mul */, ); name = MainMenu.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ CDB2CC2724D61A5000D0EECE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; }; name = Debug; }; CDB2CC2824D61A5000D0EECE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; }; name = Release; }; CDB2CC2A24D61A5000D0EECE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 4.3.1; DEVELOPMENT_TEAM = VBG97UB4TA; "DEVELOPMENT_TEAM[sdk=macosx*]" = VBG97UB4TA; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 4.3.1; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.objective-see.lulu.app"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "LuLu Application"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "LuLu Application"; SWIFT_EMIT_LOC_STRINGS = YES; }; name = Debug; }; CDB2CC2B24D61A5000D0EECE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 4.3.1; DEVELOPMENT_TEAM = VBG97UB4TA; "DEVELOPMENT_TEAM[sdk=macosx*]" = VBG97UB4TA; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 4.3.1; PRODUCT_BUNDLE_IDENTIFIER = "com.objective-see.lulu.app"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "LuLu Application"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "LuLu Application"; SWIFT_EMIT_LOC_STRINGS = YES; }; name = Release; }; CDB2CC4024D61B3900D0EECE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = Extension/Extension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 4.3.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = VBG97UB4TA; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = Extension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "LuLu Network Extension"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); LIBRARY_SEARCH_PATHS = ""; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 4.3.1; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.objective-see.lulu.extension"; PRODUCT_NAME = "com.objective-see.lulu.extension"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "LuLu Extension"; SKIP_INSTALL = YES; }; name = Debug; }; CDB2CC4124D61B3900D0EECE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = Extension/Extension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 4.3.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = VBG97UB4TA; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = Extension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "LuLu Network Extension"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); LIBRARY_SEARCH_PATHS = ""; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 4.3.1; PRODUCT_BUNDLE_IDENTIFIER = "com.objective-see.lulu.extension"; PRODUCT_NAME = "com.objective-see.lulu.extension"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "LuLu Extension"; SKIP_INSTALL = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ CDB2CC1324D61A4E00D0EECE /* Build configuration list for PBXProject "LuLu" */ = { isa = XCConfigurationList; buildConfigurations = ( CDB2CC2724D61A5000D0EECE /* Debug */, CDB2CC2824D61A5000D0EECE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CDB2CC2924D61A5000D0EECE /* Build configuration list for PBXNativeTarget "LuLu" */ = { isa = XCConfigurationList; buildConfigurations = ( CDB2CC2A24D61A5000D0EECE /* Debug */, CDB2CC2B24D61A5000D0EECE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CDB2CC3F24D61B3900D0EECE /* Build configuration list for PBXNativeTarget "Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( CDB2CC4024D61B3900D0EECE /* Debug */, CDB2CC4124D61B3900D0EECE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = CDB2CC1024D61A4E00D0EECE /* Project object */; } ================================================ FILE: LuLu/LuLu.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LuLu/LuLu.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: LuLu/LuLu.xcodeproj/xcshareddata/xcschemes/Extension.xcscheme ================================================ ================================================ FILE: LuLu/LuLu.xcodeproj/xcshareddata/xcschemes/LuLu.xcscheme ================================================ ================================================ FILE: LuLu/Shared/Localizable.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { " has a signing issue" : { "comment" : " has a signing issue", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " hat ein Signaturproblem" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "tiene un problema de firma" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "a un problème de signature" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ha un problema con la firma" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "의 서명에 문제가 있음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "ma problem z podpisem" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "tem um problema de assinatura" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : " için imzalama sorunu var" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "має проблему з підписом" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " دستخط کا مسئلہ ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "存在签名问题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "存在簽章問題" } } } }, " is not signed" : { "comment" : " is not signed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " ist nicht signiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "no está firmado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "n’est pas signé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "non è firmato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "은(는) 서명되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie jest podpisany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "não possui assinatura" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : " imzalı değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "не підписано" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " دستخط شدہ نہیں ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有簽章" } } } }, " is not validly signed" : { "comment" : " is not validly signed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " ist nicht gültig signiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "no está válidamente firmado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "n’est pas valablement signé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "non ha una firma valida" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "의 서명은 유효하지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie jest podpisany prawidłowo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "não possui assinatura válida" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : " geçerli bir biçimde imzalanmamış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "підпис недійсний" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " درست طریقے سے دستخط شدہ نہیں ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有有效的签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有有效的簽章" } } } }, " is validly signed" : { "comment" : " is validly signed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " ist gültig signiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "válidamente firmado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "est valablement signé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ha una firma valida" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "의 서명은 유효함" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "jest prawidłowo podpisany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "possui assinatura válida" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : " geçerli bir biçimde imzalanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "успішно підписано" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " درست طریقے سے دستخط شدہ ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "具有有效的签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "具有有效的簽章" } } } }, " unknown" : { "comment" : " unknown", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : " unbekannt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "desconocido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "inconnu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "sconosciuto" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : " 알 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nieznany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "desconhecido" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : " bilinmiyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "невідомий" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : " نامعلوم" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未知" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未知" } } } }, "...please copy it into /Applications and re-launch." : { "comment" : "...please copy it into /Applications and re-launch.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "…bitte kopieren Sie es in /Applications und starten Sie die Anwendung neu." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "…por favor, cópielo en /Applications y reinicie la aplicación." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "…veuillez le copier dans /Applications et relancer l'application." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "…si prega di copiarlo in /Applications e riavviare l'applicazione." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "…/Applications에 복사한 후 다시 실행해 주세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "…proszę skopiować do /Applications i ponownie uruchomić aplikację." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "…por favor, copie para /Applications e reinicie o aplicativo." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "…lütfen /Applications klasörüne kopyalayın ve uygulamayı yeniden başlatın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "…будь ласка, скопіюйте до /Applications та перезапустіть програму." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "…براہ کرم اسے /Applications میں کاپی کریں اور ایپلیکیشن دوبارہ شروع کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "…请将其复制到 /Applications 中并重新启动。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "…請將其複製到 /Applications 中並重新啟動。" } } } }, "...this will fully remove LuLu from your Mac" : { "comment" : "...this will fully remove LuLu from your Mac", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "...das wird LuLu vollständig von deinem Mac entfernen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "...esto eliminará completamente LuLu de tu Mac" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "… ceci retirera Lulu complètement de votre Mac" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "…questo rimuoverà completamente LuLu dal tuo Mac" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "...이 작업은 사용자의 Mac에서 LuLu를 완전히 삭제합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "...spowoduje to całkowite usunięcie LuLu z Twojego Maca" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "…isto vai remover LuLu completamente do seu Mac" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "…bu, LuLu’yu Mac’inizden tümüyle kaldıracaktır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "…це повністю видалить LuLu з вашого Маку" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "...یہ آپ کے میک سے لولو کو مکمل طور پر ہٹا دے گا" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "…这会将LuLu从您的Mac完全移除。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "…這會將 LuLu 從您的 Mac 完全移除 。" } } } }, "...this will terminate LuLu, until the next time you log in." : { "comment" : "...this will terminate LuLu, until the next time you log in.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "...das wird LuLu beenden, bis du dich das nächste Mal anmeldest." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "...esto terminará LuLu, hasta la próxima vez que inicies sesión." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "… cela arrêtera LuLu, jusqu'à la prochaine ouverture de session." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "…questo arresterà LuLu, fino al prossimo accesso" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "...이 작업은 다음 로그인할 때까지 LuLu의 작동을 멈춥니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "...spowoduje to zamknięcie LuLu, do czasu ponownego zalogowania." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "…isto vai encerrar LuLu, até o próximo login." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "…bu, bir sonraki oturum açışınıza dek LuLu’yu sonlandıracaktır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "…це зупинить роботу LuLu до наступного входу." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "...یہ آپ کے اگلے لاگ ان تک لولو کو ختم کر دے گا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "…这将关闭LuLu 直到您下次登入" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "…這將關閉 LuLu,直到您下次登入。" } } } }, "'%@' matches an existing profile name." : { "comment" : "'%@' matches an existing profile name.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "„%@“ stimmt mit einem vorhandenen Profilnamen überein." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "“%@” coincide con un nombre de perfil existente." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "« %@ » correspond à un nom de profil existant." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "\"%@\" corrisponde a un nome di profilo esistente." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "\"%@\"은(는) 기존 프로필 이름과 일치합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "„%@” pasuje do istniejącej nazwy profilu." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "“%@” corresponde a um nome de perfil existente." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "“%@” var olan bir profil adıyla eşleşiyor." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "«%@» збігається з існуючою назвою профілю." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "'%@' کسی موجودہ پروفائل نام سے میل کھاتا ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "“%@” 与现有的配置文件名称匹配。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "“%@” 与现有配置文件名称匹配。" } } } }, "'Default' is a reserved profile name." : { "comment" : "'Default' is a reserved profile name.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "„Default“ ist ein reservierter Profilname." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "“Default” es un nombre de perfil reservado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "« Default » est un nom de profil réservé." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "\"Default\" è un nome di profilo riservato." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "\"Default\"은 예약된 프로필 이름입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "„Default” jest zastrzeżoną nazwą profilu." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "“Default” é um nome de perfil reservado." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "“Saptanmış”, ayrılmış bir profil adıdır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "«Default» — зарезервована назва профілю." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "'Default' ایک محفوظ شدہ پروفائل نام ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "“Default” 是保留的配置文件名称。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "“Default”是保留的配置文件名称。" } } } }, "› no signing authorities" : { "comment" : "› no signing authorities", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "› keine Zertifizierungsinstanz" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "› no hay autoridades de firma" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "> pas d’autorité de signature" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "› non firmato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "› 인증 기관 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "> brak upoważnień do podpisywania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "> sem autoridades de assinatura" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "› imzalama otoritesi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "› немає довірених підписантів" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "› دستخط کرنے والے اتھارٹی نہیں ہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有签名权限" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "› 無簽署單位" } } } }, "%@ (pid: %@)" : { "comment" : "%@ (pid: %@)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid: %2$@)" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ (pid: %2$@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid: %2$@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid : %2$@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ (pid: %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid: %2$@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid: %2$@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid: %2$@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (pid: %2$@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (PID: %2$@) " } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (پی ڈی: %2$@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ (pid: %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@(pid: %@)" } } } }, "%@ (signer: %@)" : { "comment" : "%@ (signer: %@)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (Unterzeichner: %2$@)" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ (signer: %2$@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (firmante: %2$@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (signataire : %2$@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (firmato da: %2$@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (서명자: %2$@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (sygnatariusz: %2$@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (assinante: %2$@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (imzalayan: %2$@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (підписант: %2$@) " } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (دستخط کنندہ: %2$@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@(签名者:%@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ (簽署者:%@)" } } } }, "%@ (signer: Apple Mac OS App Store)" : { "comment" : "%@ (signer: Apple Mac OS App Store)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ (Unterzeichner: Apple Mac OS App Store)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ (firmante: Apple Mac OS App Store)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ (signataire : Apple Mac OS App Store)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ (firmato da: Apple Mac OS App Store)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ (서명자: Apple Mac OS App Store)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ (sygnatariusz: Sklep Aplikacji Apple Mac OS)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ (assinante: Apple Mac OS App Store)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ (imzalayan: Apple macOS App Store)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ (підписант: Apple Mac OS App Store)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ (دستخط کنندہ: ایپل میک او ایس ایپ اسٹور)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@(签名者:Apple Mac OS App Store)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@(簽署者:Apple Mac OS App Store)" } } } }, "%@ (signer: Apple Proper)" : { "comment" : "%@ (signer: Apple Proper)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ (Unterzeichner: Apple selbst)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ (firmante: Apple Original)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ (signataire : Apple)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ (firmato da: Apple Proper)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ (서명자: Apple 자체 서명)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ (sygnatariusz: firma Apple)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ (assinante: a própria Apple)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ (imzalayan: Apple)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ (підписант: Apple Proper)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ (دستخط کنندہ: ایپل پروپر)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@(签名者:Apple Proper)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@(簽署者:Apple Proper)" } } } }, "%@ (signer: invalid/unsigned)" : { "comment" : "%@ (signer: invalid/unsigned)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ (Unterzeichner: ungültig/unsigniert)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ (firmante: no válido/no firmado)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ (signataire : invalide/non signé)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ (firmato da: Non Valido/Non Firmato)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ (서명자: 유효하지 않음/서명되지 않음)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ (sygnatariusz: nieprawidłowy/nieprzypisany)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ (assinante: inválido/sem assinatura)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ (imzalayan: Geçersiz/İmzasız)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ (підписант: невалідний/не підписано)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ (دستخط کنندہ: غیر معتبر/غیر دستخط شدہ)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@(签名者:无效/未签名)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@(簽署者:無效/未簽署)" } } } }, "%@ does not exist!" : { "comment" : "%@ does not exist!", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ existiert nicht!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ no existe!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ n'existe pas !" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ non esiste!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@은(는) 존재하지 않습니다!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ nie istnieje!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ não existe!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ yok!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ не існує!" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ موجود نہیں ہے!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 不存在!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 不存在!" } } } }, "%@ is legitimate macOS process" : { "comment" : "%@ is legitimate macOS process", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ ist ein legitimer macOS-Prozess" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ es un proceso legítimo de macOS " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ est un processus macOS légitime" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ è un processo legittimo di macOS" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@은(는) 적법한 macOS 프로세스입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ jest prawidłowym procesem macOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ é um processo legítimo do macOS" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ geçerli bir macOS işlemi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ є легітимним процесом macOS" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ ایک قانونی میکس او ایس پروسس ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@是合法的macOS程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 是合法的 macOS 程式" } } } }, "%@ is not a valid (port) number" : { "comment" : "%@ is not a valid (port) number", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ ist keine gültige (Port-)Nummer" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ no es un número válido (de puerto)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ n'est un numéro (de port) valide" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ non è un numero (di porta) valido" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@은(는) 유효하지 않은 (포트) 번호입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ nie jest prawidłowym numerem (portu)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ não é um número (de porta) válido " } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ geçerli bir kapı numarası değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ не є валідним номером порту" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ ایک درست (پورٹ) نمبر نہیں ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 不是有效的(端口)号" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 不是有效的(連接埠)號碼" } } } }, "%@ is not a valid regular expression\r\ndetails: %@" : { "comment" : "%@ is not a valid regular expression\r\ndetails: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ ist kein gültiger regulärer Ausdruck\r\nDetails: %2$@" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ is not a valid regular expression\r\ndetails: %2$@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ no es una expresión regular válida\r\ndetalles: %2$@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ n’est pas une expression régulière valide\ndetails : %2$@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ non è un’espressione regolare valida\ndettagli: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$@은(는) 유효하지 않는 정규 표현식입니다\n세부 정보: %2$@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ nie jest prawidłowym wyrażeniem regularnym szczegóły: %2$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ não é uma expressão regular válida. Detalhes: %2$@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ geçerli bir düzenli ifade değil, ayrıntılar: %2$@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ не є валідним регулярним виразом \nДеталі: %2$@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ ایک درست ریگولر ایکسپریشن نہیں ہے\nتفصیلات: %2$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 不是有效的正则表达式\n详细信息:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ 不是有效的正則表達式\n詳細資訊:%2$@" } } } }, "%@ signed by 3rd-party/ad hoc" : { "comment" : "%@ signed by 3rd-party/ad hoc", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ von Drittanbieter/ad hoc signiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmado por un tercero/ad hoc" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ signé par une tierce-partie/ad hoc" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ è firmato da " } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 은(는) 서드파티/Ad hoc 서명됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ podpisane przez stronę trzecią/doraźnie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ assinado por terceiros/ad hoc" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ 3. kişiler/geçici kişi tarafından imzalanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ має неофіційний (ad hoc) підпис" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ تیسری پارٹی/ایڈ ہاک کے ذریعہ دستخط شدہ ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由第三方/临时签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由第三方/臨時簽署" } } } }, "%@ signed by Apple proper" : { "comment" : "%@ signed by Apple proper", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ signiert von Apple selbst" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmado por Apple oficialmente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ signé par Apple" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmato correttamente da Apple" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ Apple에 의해 자체 서명됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ podpisane przez firmę Apple" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ assinado pela própria Apple" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ Apple tarafından düzgünce imzalanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ підписано безпосередньо Apple" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ ایپل پروپر کے ذریعہ دستخط شدہ ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由 Apple 正式签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由 Apple Proper 簽署" } } } }, "%@ signed by Mac App Store" : { "comment" : "%@ signed by Mac App Store", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ signiert vom Mac App Store" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmado por Mac App store" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ signé par le Mac App Store" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmato da Mac App Store" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ Mac App Store에 의해 서명됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ podpisane przez Mac App Store" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ assinado pela App Store no Mac" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ Mac App Store tarafından imzalanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ підписано Mac App Store" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ میک ایپ اسٹور کے ذریعہ دستخط شدہ ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由 Mac App Store 签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由 Mac App Store 簽署" } } } }, "%@ signed with an Apple Developer ID" : { "comment" : "%@ signed with an Apple Developer ID", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ signiert mit einer Apple Developer ID" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmado con un ID de Desarrollador de Apple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ signé avec un Apple Developer ID" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ firmato da un Apple Developer ID" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ Apple Developer ID에 의해 서명됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ podpisano za pomocą Apple Developer ID" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ assinado com ID de Desenvolvidor Apple" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ bir Apple Geliştirici Kimliği ile imzalanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ підписано з Apple Developer ID" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ ایپل ڈویلپر آئی ڈی کے ذریعہ دستخط شدہ ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 使用 Apple 开发者 ID 签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 由 Apple 開發者 ID 簽署" } } } }, "%@ signing error: %#lx" : { "comment" : "%@ signing error: %#lx", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ Signaturfehler: %#lx" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ error de firma: %#lx" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ erreur de signature : %#lx" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ errore di firma: %#lx" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 서명 에러: %#lx" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ błąd podpisywania: %#lx" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ erro de assinatura: %#lx" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ imzalama hatası: %#lx" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "помилка підпису: %#lx" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ دستخطی خطا: %#lx" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 签名错误:%#lx" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 簽署錯誤:%#lx" } } } }, "%@ this rule, may impact legitimate system functionalty ...continue?" : { "comment" : "%@ this rule, may impact legitimate system functionalty ...continue?", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ diese Regel könnte die legitime Systemfunktionalität beeinträchtigen ...fortfahren?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ esta regla, puede impactar la funcionalidad legítima del sistema...¿continuar?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ cette règle, peut impacter le fonctionnement légitime du système ... continuer ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ questa regola potrebbe impattare alcune funzionalità di sistema ...continuare?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 작업 시, 적법한 시스템 기능에 영향이 있을 수 있습니다. 계속할까요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ ta reguła, może mieć wpływ na prawidłowe funkcjonowanie systemu...kontynuować?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ esta regra, pode impactar funcionalidade legítima do sistema …prosseguir?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "bu kuralı %@ yapmak, geçerli sistem işlevselliğini etkileyebilir …sürdürülsün mü?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ це правило може вплинути на коректну роботу системи... Продовжити?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ اس قاعدہ سے، قانونی سسٹم کی کارکردگی پر اثر پڑ سکتا ہے ... جاری رکھیں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 此规则可能会影响合法系统功能…要继续吗?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 此規則可能影響系統的正常功能…要繼續嗎?" } } } }, "%@'s code signing information has changed" : { "comment" : "%@'s code signing information has changed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@s Codesignierungsinformationen haben sich geändert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "la información de firma de código de %@ ha cambiado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les informations de signature de %@ ont changé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La firma di %@ è cambiata" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@의 디지털 서명 정보가 변경되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ Informacje o podpisie kodu uległy zmianie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : " %@ teve mudanças na informação da sua assinatura de código" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ öğesinin kod imzalama bilgisi değiştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інформація щодо підпису коду %@ змінилася" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ کی کوڈ دستخط کی معلومات تبدیل ہو گئی ہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 的代码签名信息已发生改变" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 的程式簽署資訊已變更" } } } }, "" : { "comment" : "", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "<(%d) inconnu>" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "<알 수 없음 (%d)>" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "<невідомий (%d)>" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "<نامعلوم (%d)>" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "<未知(%d)>" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "" } } } }, "2. In System Settings, scroll to where it mentions 'LuLu' and click 'Allow'" : { "comment" : "2. In System Settings, scroll to where it mentions 'LuLu' and click 'Allow'", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "2. Scrolle in den Systemeinstellungen zu der Stelle, an der „LuLu“ erwähnt wird und klicke auf „Erlauben“." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "2. En Configuración del Sistema, deslice hasta 'LuLu' y haga click en 'Permitir'" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "2. Dans Réglages Systèmes, descendez jusqu’à 'LuLu' et cliquez sur 'Approuver'" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "2. Nelle Impostazioni di Sistema, scorri fino alla voce “LuLu” e clicca su “Consenti”" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "2. 시스템 설정에서 'LuLu'가 있는 곳으로 스크롤하여 '허용'을 클릭합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "2. W Ustawieniach systemowych przewiń do miejsca, w którym wspomniano o „LuLu” i kliknij „Zezwól”" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "2. Nas Preferências do Sistema, role até encontrar ‘LuLu’ e clique em ‘Permitir’" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "2. Sistem Ayarları’nda, “LuLu”dan söz ettiği yere sarın ve “İzin Ver”e tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "2. У «Системних параметрах» прокрутіть до LuLu і натисніть «Дозволити»." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "2. سسٹم سیٹنگز میں، 'LuLu' کے ذکر کردہ مقام پر اسکرول کریں اور 'الاؤ' پر کلک کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "2. 在系统设置中,滚动到提到“LuLu”的位置,然后单击“允许”" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "2. 在「系統設定」中,捲動至提及「LuLu」的部分,然後點擊「允許」" } } } }, "2. In System Settings, toggle on the LuLu extension" : { "comment" : "2. In System Settings, toggle on the LuLu extension", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "2. Schalte die LuLu-Erweiterung in den Systemeinstellungen ein." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "2. En Configuración del Sistema, active la extensión de Lulu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "2. Dans Réglages Systèmes, activer l’extension LuLu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "2. Nelle Impostazioni di Sistema, abilita l’estensione LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "2. 시스템 설정에서 LuLu 확장 프로그램을 켭니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "2. W Ustawieniach systemowych włącz rozszerzenie LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "2. Nas Preferências do Sistema, clique na extensão do LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "2. Sistem Ayarları’nda, LuLu genişletmesini açın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "2. У «Системних параметрах» увімкніть розширення LuLu." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "2. سسٹم سیٹنگز میں، LuLu ایکسٹینشن کو آن کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "2. 在系统设置中,打开 LuLu 扩展程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "2. 在「系統設定」中,開啟 LuLu 延伸功能" } } } }, "a new version (%@) is available!" : { "comment" : "a new version (%@) is available!", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "eine neue Version (%@) ist verfügbar!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡una nueva versión (%@) está disponible!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "une nouvelle version (%@) est disponible !" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "una nuova versione (%@) è disponibile!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 버전(%@)이 있습니다!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "dostępna jest nowa wersja (%@)!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "uma nova versão (%@) está disponível!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "yeni bir sürüm (%@) kullanılabilir!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "доступна нова версія (%@)!" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایک نئی ورژن (%@) دستیاب ہے!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "有新版本(%@)可用!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "有可用的新版本(%@)!" } } } }, "Ad hoc" : { "comment" : "Ad hoc", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ad hoc" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ad hoc" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ad hoc" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "A questo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Ad hoc" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Doraźnie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ad Hoc" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçici" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ad hoc" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایڈ ہاک" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "临时签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "臨時簽署" } } } }, "Add Profile" : { "comment" : "Add Profile", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar perfil" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un profil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi profilo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj profil" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar perfil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати профіль" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل شامل کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "添加配置文件" } } } }, "Added in last 24 hours (sorted by creation time)" : { "comment" : "Added in last 24 hours (sorted by creation time)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "In den letzten 24 Stunden hinzugefügt (nach Erstellungszeit sortiert)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregado en las últimas 24 horas (ordenado por hora de creación)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajoutés ces dernières 24 heures (trié par date de création)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiunte nelle ultime 24 ore (ordinate per data di creazione)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "최근 24시간 이내에 추가됨 (생성 시간 순 정렬)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodano w ciągu ostatnich 24 godzin (posortowane według czasu utworzenia)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionado nas últimas 24h (ordenados por hora de criação)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Son 24 saat içinde eklendi (oluşturulma zamanına göre sıralı)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : " Додано за останні 24 години (відсортовано за часом створення)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "گذشتہ 24 گھنٹوں میں شامل کیا گیا (تخلیق کے وقت کے مطابق ترتیب دیا گیا)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "过去 24 小时内添加(按创建时间排序)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "過去 24 小時內新增(依建立時間排序)" } } } }, "Added passively (either if 'passive mode' is set, or no user was logged in when rule was created)" : { "comment" : "Added passively (either if 'passive mode' is set, or no user was logged in when rule was created)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Passiv hinzugefügt (entweder wenn der „Passive Modus“ aktiviert ist oder kein Benutzer angemeldet war, als die Regel erstellt wurde)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregado pasivamente (cuando el “modo pasivo” está activado o ningún usuario había iniciado sesión cuando se creó la regla)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouté passivement (si le « mode passif » est activé ou si aucun utilisateur n’était connecté lors de la création de la règle)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiunto passivamente (se è attivata la “modalità passiva” o nessun utente era connesso quando la regola è stata creata)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "수동으로 추가됨 (’수동 모드’가 설정되어 있거나 규칙 생성 시 로그인한 사용자가 없을 경우)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodano pasywnie (gdy ustawiono „tryb pasywny” lub żaden użytkownik nie był zalogowany w momencie tworzenia reguły)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionado passivamente (quando o “modo passivo” está ativado ou nenhum usuário estava logado quando a regra foi criada)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pasif olarak eklendi (“pasif mod” etkinse veya kural oluşturulurken hiçbir kullanıcı oturum açmamışsa)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додано пасивно (якщо увімкнено «пасивний режим» або під час створення правила жоден користувач не був увійшов у систему)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "غیر فعال طور پر شامل کیا گیا (یا تو اگر \"غیر فعال موڈ\" سیٹ ہے، یا جب اصول بنایا گیا تو کوئی صارف لاگ ان نہیں تھا)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "被动添加(当设置了“被动模式”,或在创建规则时没有用户登录时)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "被动添加(当启用“被动模式”或创建规则时没有用户登录)" } } } }, "Added Profile" : { "comment" : "Added Profile", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil hinzugefügt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil agregado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil ajouté" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo aggiunto" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가된 프로필" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodany profil" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil adicionado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklenen Profil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доданий профіль" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "حذف کی تصدیق کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已添加的配置文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已添加的配置文件" } } } }, "All Rules" : { "comment" : "All Rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Regeln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todas las Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Toute les règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutte le Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystkie reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Todas as Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Всі правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "تمام قواعد" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全部规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全部規則" } } } }, "Allow" : { "comment" : "@Allow\nAllow", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erlauben" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "허용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwól" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin Ver" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "الاؤ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許" } } } }, "Allow (pid: %@)" : { "comment" : "Allow (pid: %@)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erlauben (pid: %@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir (pid: %@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser (pid : %@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti (pid: %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "허용 (pid: %@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwól (pid: %@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir (pid: %@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin Ver (PID: %@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити (PID: %@)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "الاؤ (پی آئی ڈی: %@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许(pid: %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許(pid: %@)" } } } }, "Allow (until: %@)" : { "comment" : "Allow (until: %@)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erlauben (bis: %@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permitir (hasta: %@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autoriser (jusqu’à : %@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Consenti (fino a: %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "허용 (기한: %@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zezwól (do: %@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permitir (até: %@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin Ver (Şuna dek: %@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволити (до: %@)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "الاؤ (تک: %@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "允许(直到: %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "允許(直到:%@)" } } } }, "any address" : { "comment" : "any address", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "jede Adresse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "cualquier dirección" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "toute adresse" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ogni indirizzo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 주소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "dowolny adres" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "qualquer endereço" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "herhangi bir adres" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "будь-яка адреса" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کوئی بھی پتہ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "任意地址" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "任何位址" } } } }, "any port" : { "comment" : "any port", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "jeder Port" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "cualquier puerto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "tout port" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ogni porta" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 포트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "dowolny port" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "qualquer porta" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "herhangi bir kapı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "будь-який порт" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کوئی بھی پورٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "任意端口" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "任何連接埠" } } } }, "Any program" : { "comment" : "Any program", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Jedes Programm" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cualquier programa" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout programme" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ogni programma" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 프로그램" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Jakikolwiek program" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Qualquer programa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "herhangi bir program" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Будь-яка програма" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کوئی بھی پروگرام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "任意程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "任何程式" } } } }, "Apple Programs (automatically allowed & added here if 'allow apple programs' is set)" : { "comment" : "Apple Programs (automatically allowed & added here if 'allow apple programs' is set)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apple-Programme (automatisch zugelassen und hier hinzugefügt, wenn „Apple-Programme erlauben“ aktiviert ist)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Programas de Apple (permitidos automáticamente y agregados aquí si está configurado 'permitir programas de Apple')" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Programmes Apple (automatiquement approuvé et ajouté ici si 'Autoriser les programmes Apple' est activé)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programmi Apple (autorizzati e aggiunti qui automaticamente se è impostato \"Consenti programmi Apple\")" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Apple 프로그램 ('Apple 프로그램 허용' 설정에 따라 자동으로 허용 및 추가됨)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Programy Apple (automatycznie dozwolone i dodawane tutaj, jeśli ustawiono opcję „zezwól programom Apple”)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Programas da Apple (automaticamente adicionados aqui se ‘permitir programas Apple’ está selecionado)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Apple Programları (“Apple programlarına izin ver” ayarlıysa buraya kendiliğinden eklenir ve izin verilir)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програми Apple (автоматично дозволяються й додаються сюди, якщо увімкнено «Дозволяти програми Apple»)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایپل پروگرامز (اگر 'ایپل پروگرامز کو الاؤ' سیٹ کیا جائے تو خودکار طور پر یہاں شامل اور الاؤ دیا جاتا ہے)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Apple 程序(如果设置了“允许 Apple 程序”,则自动允许并添加到此处)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Apple 程式(若啟用「允許 Apple 程式」,則會自動允許並新增至此)" } } } }, "Block" : { "comment" : "Block", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bloquer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Blocca" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Blokuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Блокувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋" } } } }, "Block (pid: %@)" : { "comment" : "Block (pid: %@)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockieren (pid: %@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear (pid: %@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bloquer (pid : %@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Blocca (pid: %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단 (pid: %@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Blokuj (pid: %@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear (pid: %@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle (PID: %@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Блокувати (PID: %@)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک کریں (پی آئی ڈی: %@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止 (pid: %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋(pid: %@)" } } } }, "Block (until: %@)" : { "comment" : "Block (until: %@)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Blockieren (bis: %@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear (hasta: %@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bloquer (jusqu’à : %@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Blocca (fino a: %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "차단 (기한: %@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Blokuj (do: %@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bloquear (até: %@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle (Şuna dek: %@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Блокувати (до: %@)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "بلاک کریں (تک: %@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阻止 (直到: %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "阻擋 (直到:%@)" } } } }, "Cancel" : { "comment" : "Cancel", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abbrechen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annuler" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cancella" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "취소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Anuluj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Vazgeç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "منسوخ کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消" } } } }, "Clean up rules referencing deleted, expired, or terminated items?" : { "comment" : "Clean up rules referencing deleted, expired, or terminated items?", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln bereinigen, die auf gelöschte, abgelaufene oder beendete Elemente verweisen?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Reglas de limpieza que hacen referencia a elementos eliminados, vencidos o finalizados?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyer les règles faisant référence à des éléments supprimés, expirés ou terminés ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vuoi pulire le regole che fanno riferimento agli elementi eliminati, scaduti o terminati?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제, 만료, 또는 중지된 항목을 참조하는 규칙을 정리할까요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czy reguły czyszczenia odnoszą się do elementów usuniętych, wygasłych lub zamkniętych?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpar regras que fazem referência a itens excluídos, expirados ou encerrados?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Silinen, süresi dolan veya sonlandırılan öğelere atıfta bulunan temizleme kurallarını mı istiyorsunuz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити правила, що посилаються на видалені, прострочені або завершені елементи?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "حذف شدہ، ختم شدہ یا ختم شدہ اشیاء کے حوالے سے قواعد کو صاف کرنا چاہتے ہیں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清理引用已删除、过期或终止的项目的规则?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清理引用已刪除、過期或終止項目的規則?" } } } }, "Cleaned up %ld rules" : { "comment" : "Cleaned up %ld rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%ld Regeln aufgeräumt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Se limpiaron %ld reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%ld règles nettoyées" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%ld regole pulite" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%ld개의 규칙을 정리함" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyczyszczono %ld reguł" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "As regras de %ld foram limpas" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%ld kural temizlendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалено %ld правил" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%ld قواعد کو صاف کیا گیا" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已清理 %ld 条规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已清理 %ld 條規則" } } } }, "Confirm Deletion" : { "comment" : "Confirm Deletion", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Löschung bestätigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Confirmar eliminación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Confirmer la suppression" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Conferma eliminazione" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제 확인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Potwierdź usunięcie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Confirmar exclusão" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Silmeyi Onayla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтвердити видалення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "حذف کی تصدیق کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "确认删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "确认删除" } } } }, "Continue" : { "comment" : "Continue", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fortfahren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Continuar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Continuer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Continua" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "계속" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kontynuuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Coninuar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürdür" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Продовжити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "جاری رکھیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "继续" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "繼續" } } } }, "Current profile is now: '%@'." : { "comment" : "Current profile is now: '%@'.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktuelles Profil ist nun: „%@“." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El perfil actual ahora es: “%@”." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le profil actuel est désormais : « %@ »." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il profilo attuale è ora: \"%@\"." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "현재 프로필은 \"%@\"입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualny profil to: „%@”." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O perfil atual agora é: “%@”." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçerli profil şimdi: “%@”." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Поточний профіль: «%@»." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "موجودہ پروفائل اب ہے: '%@'۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前配置文件为:“%@”。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "当前配置文件为:”%@”。" } } } }, "Current Profile: %@" : { "comment" : "Current Profile: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktuelles Profil: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil actual: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil actuel : %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo attuale: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "현재 프로필: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualny profil: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil atual: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçerli profil: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Поточний профіль: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "موجودہ پروفائل: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前配置文件:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "当前配置文件:%@" } } } }, "Current Profile: Default" : { "comment" : "Current Profile: Default", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktuelles Profil: Default" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil actual: Default" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil actuel : Default" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo attuale: Default" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "현재 프로필: Default" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualny profil: Default" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil atual: Default" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçerli profil: Saptanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Поточний профіль: Default" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "موجودہ پروفائل: Default" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前配置文件:Default" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "当前配置文件:Default" } } } }, "Default" : { "comment" : "Default", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Saptanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "默认" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Default" } } } }, "Delete" : { "comment" : "Delete", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "حذف کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除" } } } }, "Delete %@" : { "comment" : "Delete %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sil: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ حذف کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "删除%@" } } } }, "Delete profile: '%@'?" : { "comment" : "Delete profile: '%@'?", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil „%@“ löschen?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Eliminar el perfil “%@”?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le profil : « %@ » ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Eliminare il profilo \"%@\"?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 \"%@\"을(를) 삭제하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usunąć profil „%@”?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir o perfil “%@”?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "“%@” profili silinsin mi?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити профіль «%@»?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل '%@' کو حذف کریں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除配置文件:“%@”?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "删除配置文件:”%@”?" } } } }, "Delete Rule?" : { "comment" : "Delete Rule?", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regel löschen?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Eliminar regla?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer la règle ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Eliminare la regola?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙을 삭제하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usunąć regułę?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir regra?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural silinsin mi?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити правило?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "قاعدہ حذف کریں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除规则?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除規則?" } } } }, "Directory Rules apply to all items within the directory" : { "comment" : "Directory Rules apply to all items within the directory", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verzeichnisregeln gelten für alle Objekte innerhalb des Verzeichnisses" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Las Reglas de Directorio se aplican a todos los elementos dentro del directorio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les règles de répertoire s'appliquent à tous les éléments du répertoire" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le regole della directory si applicano a tutti gli elementi all'interno della directory" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "디렉토리 규칙은 디렉토리 내 모든 항목에 적용됩니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły katalogu dotyczą wszystkich elementów w katalogu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras do Diretório se aplicam à todos os itens contidos no diretório" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dizin kuralları, dizindeki tüm öğelere uygulanır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила каталогів застосовуються до всіх елементів у каталозі." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ڈائریکٹری قواعد ڈائریکٹری کے اندر موجود تمام اشیاء پر لاگو ہوتی ہیں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "目录规则适用于目录内的所有项目" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "目錄規則適用於該目錄內的所有項目" } } } }, "Disable" : { "comment" : "Disable", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deaktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desactivado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilita" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desabilitar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Devre Dışı Bırak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "غیر فعال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用" } } } }, "Disable %@" : { "comment" : "Disable %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ deaktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desactivar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilita %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desativar %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Devre Dışı Bırak: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ کو غیر فعال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "禁用%@" } } } }, "Display Path(s)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad/Pfade anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar ruta(s)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher le(s) chemin(s)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra percorso/i" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl ścieżkę(i)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exibir caminho(s)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yol(ları) Göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати шлях(и)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "راستہ(جات) دکھائیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "显示路径" } } } }, "Edit %@" : { "comment" : "Edit %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ bearbeiten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Editar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Modifier %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modifica %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 편집" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Edytuj %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Editar %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Düzenle: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Редагувати %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ میں ترمیم کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "编辑 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "编辑%@" } } } }, "Enable" : { "comment" : "Enable", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Activado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "فعال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用" } } } }, "Enable %@" : { "comment" : "Enable %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Activar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etkinleştir: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ کو فعال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "启用%@" } } } }, "ERROR: activation failed" : { "comment" : "ERROR: activation failed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Aktivierung fehlgeschlagen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: activación fallida" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ERREUR: échec de l’activation" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Attivazione fallita" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 활성화 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: aktywacja nie powiodła się" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: ativação falhou" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Etkinleştirme başarısız" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: активація не вдалася" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خطا: فعال کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:激活失败" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:啟動失敗" } } } }, "ERROR: Enter a non-zero duration" : { "comment" : "ERROR: Enter a non-zero duration", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Geben Sie eine Dauer ungleich Null ein" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: Introduzca una duración distinta de cero" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ERREUR : Entrez une durée non nulle" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Inserire una durata diversa da zero" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 0이 아닌 지속 시간을 입력하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: Wprowadź niezerowy czas trwania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Insira uma duração diferente de zero" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Sıfırdan farklı bir süre girin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: Введіть ненульову тривалість" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: صفر سے مختلف دورانیہ درج کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:请输入非零的持续时间" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:請輸入非零的持續時間" } } } }, "ERROR: Failed to cleanup rules" : { "comment" : "ERROR: Failed to cleanup rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Aufräumen der Regeln fehlgeschlagen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: Fallo al limpiar las reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : échec du nettoyage des règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Pulizia regole non completata" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 규칙 정리 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: Nie udało się wyczyścić reguł" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Falha ao limpar regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Kurallar temizlenemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: не вдалося видалити правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: اصولوں کی صفائی میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:无法清理规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:清理規則失敗" } } } }, "ERROR: Failed to export rules" : { "comment" : "ERROR: Failed to export rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Exportieren der Regeln fehlgeschlagen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: Fallo al exportar las reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : échec de l'exportation des règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Esportazione delle regole non completata" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 규칙 내보내기 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: Nie udało się wyeksportować reguł" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Falha na exportação de regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Kurallar dışa aktarılamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: не вдалося експортувати правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: اصولوں کو برآمد کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:无法导出规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:規則匯出失敗" } } } }, "ERROR: Failed to hash %@" : { "comment" : "ERROR: Failed to hash %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Hashing von %@ fehlgeschlagen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: No se pudo generar el hash de %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ERREUR : Échec du hachage de %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Impossibile generare l’hash di %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: %@ 해시 생성 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: Nie udało się wygenerować skrótu %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Falha ao gerar o hash de %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: %@ karması oluşturulamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: Не вдалося обчислити хеш %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: %@ کا ہیش بنانے میں ناکام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:哈希%@失败" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:雜湊%@失敗" } } } }, "ERROR: Failed to import rules" : { "comment" : "ERROR: Failed to import rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Importieren der Regeln fehlgeschlagen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: Fallo al importar las reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : échec de l'importation des règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Importazione delle regole non completata" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 규칙 가져오기 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: Nie udało się zaimportować reguł" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Falha na importação de regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Kurallar içe aktarılamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: не вдалося імпортувати правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: اصولوں کو درآمد کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:无法导入规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:規則匯入失敗" } } } }, "ERROR: failed to load patrons" : { "comment" : "ERROR: failed to load patrons", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Unterstützer konnten nicht geladen werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: fallo al cargar patrones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : échec du chargement des patrons" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Caricamento patrons non completato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: Patron 후원자 불러오기 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: nie udało się załadować patronów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Falha ao carregar patrocinadores" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Aboneler yüklenemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: не вдалося завантажити список патронів" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: سرپرستوں کو لوڈ کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:无法加载赞助者" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:載入贊助者失敗" } } } }, "ERROR: invalid path" : { "comment" : "ERROR: invalid path", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Ungültiger Pfad" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: ruta no válida" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : chemin invalide" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Percorso non valido" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 유효하지 않은 경로" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: nieprawidłowa ścieżka" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Caminho inválido" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Geçersiz yol" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: некоректний шлях" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: نا درست راستہ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:路径无效" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:無效的路徑" } } } }, "ERROR: invalid port" : { "comment" : "ERROR: invalid port", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Ungültiger Port" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: puerto no válido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : port invalide" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Porta non valida" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 유효하지 않은 포트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: nieprawidłowy port" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Porta inválida" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Geçersiz kapı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: некоректний порт" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: نا درست پورٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:端口无效" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:無效的連接埠" } } } }, "ERROR: invalid regex" : { "comment" : "ERROR: invalid regex", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "FEHLER: Ungültiger regulärer Ausdruck" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ERROR: regex no válido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : regex invalide" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ERRORE: Espressione regolare non corretta" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 유효하지 않은 정규 표현식" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "BŁĄD: nieprawidłowe wyrażenie regularne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Expressão regular inválida" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Geçersiz düzenli ifade" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: некоректний регулярний вираз" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: نا درست ریگولر ایکسپریشن" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:正则表达式无效" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:無效的正則表達式" } } } }, "error: update check failed" : { "comment" : "error: update check failed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fehler: Prüfen auf Updates fehlgeschlagen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "error: falló la verificación de actualización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur : la recherche de mise à jour a échoué" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "errore: impossibile controllare la disponibilità di aggiornamenti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류: 업데이트 확인 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "błąd: nie udało się sprawdzić aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ERRO: Falha na verificação de update" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "HATA: Güncelleme denetimi başarısız" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ПОМИЛКА: не вдалося перевірити оновлення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "خرابی: اپ ڈیٹ چیک میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误:更新检查失败" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤:更新檢查失敗" } } } }, "Extensions must be manually approved via System Settings (General > Login Items & Extensions > Network Extensions)." : { "comment" : "Extensions must be manually approved via System Settings (General > Login Items & Extensions > Network Extensions).", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erweiterungen müssen manuell über die Systemeinstellungen (Allgemein > Anmeldeobjekte und Erweiterungen > Netzwerkerweiterungen) genehmigt werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Las extensiones deben aprobarse manualmente a través de Configuración del sistema (General > Elementos de inicio de sesión y extensiones > Extensiones de red)." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les extensions doivent être approuvées manuellement via les paramètres système (Général > Éléments de connexion et extensions > Extensions réseau)." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le estensioni devono essere approvate manualmente tramite Impostazioni di sistema (Generale > Elementi di accesso ed estensioni > Estensioni di rete)." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확장 프로그램은 시스템 설정(일반 > 로그인 항목 및 확장 프로그램 > 네트워크 확장 프로그램)을 통해 수동으로 승인되어야 합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozszerzenia muszą zostać zatwierdzone ręcznie w Ustawieniach systemowych (Ogólne > Rzeczy i rozszerzenia otwierane podczas logowania > Rozszerzenia sieciowe)." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "As extensões devem ser aprovadas manualmente por meio das Configurações do sistema (Geral > Itens de login e extensões > Extensões de rede)." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzantılar, Sistem Ayarları (Genel > Oturum Açma Öğeleri ve Uzantılar > Ağ Uzantıları) üzerinden manuel olarak onaylanmalıdır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розширення потрібно схвалити вручну в «Налаштуваннях безпеки» (Загальні > Автозапуск і розширення > Мережеві розширення)." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ایکسٹینشنز کو مینوئل طور پر سسٹم سیٹنگز (جنرل > لاگ ان آئٹمز اینڈ ایکسٹینشنز > نیٹ ورک ایکسٹینشنز) کے ذریعے منظور کرنا ہوگا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "必须通过系统设置(通用 > 登录项和扩展 > 网络扩展)手动批准扩展。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "必須在「系統設定」(一般 > 登入項目與延伸功能 > 網路延伸功能)中手動核准延伸功能。" } } } }, "failed to activate network extension" : { "comment" : "failed to activate network extension", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Netzwerkerweiterung konnte nicht aktiviert werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "falló al activar la extensión de red" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "échec de l’activation de l’extension du réseau" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impossibile attivare l’estensione di rete" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "네트워크 확장 프로그램 활성화 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie udało się aktywować rozszerzenia sieciowego" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "falha ao tentar ativar extensão de rede" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "ağ genişletmesi etkinleştirilemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "не вдалося активувати мережеве розширення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نیٹ ورک ایکسٹینشن کو فعال کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法激活网络扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動網路延伸功能失敗" } } } }, "failed to activate network filter" : { "comment" : "failed to activate network filter", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Netzwerkfilter konnte nicht aktiviert werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "falló al activar el filtro de red" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "échec de l’activation du filtre réseau" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impossibile attivare il filtro di rete" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "네트워크 필터 활성화 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie udało się aktywować filtra sieciowego" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "falha ao tentar ativar filtro de rede" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "ağ süzgeci etkinleştirilemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "не вдалося активувати мережевий фільтр" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نیٹ ورک فلٹر کو فعال کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法激活网络过滤器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用網路過濾器失敗" } } } }, "failed to activate system extension" : { "comment" : "failed to activate system extension", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Systemerweiterung konnte nicht aktiviert werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "falló al activar la extensión del sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "échec de l’activation de l’extension système" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impossibile attivare l’estensione di sistema" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템 확장 프로그램 활성화 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie udało się aktywować rozszerzenia systemu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "falha ao tentar ativar extensão do sistema" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "sistem genişletmesi etkinleştirilemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "не вдалося активувати системне розширення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "سسٹم ایکسٹینشن کو فعال کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法激活系统扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動系統延伸功能失敗" } } } }, "failed to activate system/network extension" : { "comment" : "failed to activate system/network extension", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "System-/Netzwerkerweiterung konnte nicht aktiviert werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "falló al activar la extensión del sistema/red" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "échec de l’activation de l’extension système/réseau" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impossibile attivare l’estensione di sistema/rete" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템/네트워크 확장 프로그램 활성화 실패" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie udało się aktywować rozszerzenia systemowego/sieciowego" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "falha ao tentar ativar extensão de sistema/rede" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "sistem/ağ genişletmesi etkinleştirilemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "не вдалося активувати системне або мережеве розширення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "سسٹم/نیٹ ورک ایکسٹینشن کو فعال کرنے میں ناکامی" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法激活系统/网络扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動系統/網路延伸功能失敗" } } } }, "Full URL: %@" : { "comment" : "Full URL: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vollständige URL: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "URL completo: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "URL complète: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "URL Completo: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전체 URL: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pełny adres URL: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "URL completo: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tam URL: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повний URL: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مکمل یو آر ایل: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "完整网址:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "完整網址:%@" } } } }, "Global Rules apply to all paths" : { "comment" : "Global Rules apply to all paths", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Globale Regeln gelten für alle Pfade" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Las Reglas Globales se aplican a todas las rutas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les Règles Globales s’appliquent à tous les chemins" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le Regole Globali sono valide per tutti i percorsi" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전역 규칙은 모든 경로에 적용됩니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły globalne dotyczą wszystkich ścieżek" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras Globais se aplicam a todos os caminhos" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Global kurallar tüm yollara uygulanır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Глобальні правила застосовуються до всіх шляхів" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "گلوبل رولز تمام راستوں پر لاگو ہوتے ہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全局规则适用于所有路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全域規則適用於所有路徑" } } } }, "Imported %ld rules" : { "comment" : "Imported %ld rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%ld Regeln importiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas %ld importadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%ld règles importées" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%ld regole importate" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%ld개의 규칙을 가져옴" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaimportowano %ld reguł" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%ld regras importadas" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%ld kural içe aktarıldı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Імпортовано %ld правил" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%ld رولز درآمد کیے گئے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已导入 %ld 条规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已匯入 %ld 條規則" } } } }, "Installed version (%@),\r\nis the latest." : { "comment" : "Installed version (%@),\r\nis the latest.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Die installierte Version (%@)\r\nist die neuste." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La versión instalada (%@), es la más reciente." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La version installée (%@) est la plus récente." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La versione installata (%@) è l’ultima disponibile" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치된 버전(%@)이 최신입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalowana wersja (%@),\njest najnowsza." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão instalada (%@), é a mais recente" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "kurulu sürüm (%@), günceldir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановлена версія (%@) є найновішою." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "انسٹال ورژن (%@)،\r\nسب سے تازہ ترین ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已安装版本 (%@),\n是最新版本。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已安裝版本(%@)為最新版本。" } } } }, "Invalid Profile Name" : { "comment" : "Invalid Profile Name", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ungültiger Profilname" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre de perfil no válido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom de profil invalide" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome profilo non valido" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "잘못된 프로필 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nieprawidłowa nazwa profilu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome de perfil inválido" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçersiz Profil Adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Неправильна назва профілю" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "غلط پروفائل نام" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名称无效" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名称无效" } } } }, "is connecting to %@" : { "comment" : "is connecting to %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "verbindet sich mit %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "se está conectando a %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "se connecte à %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "si sta connettendo a %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스가 다음으로 연결 시도 중: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "łączy się z %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "está conectando a %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ konumuna bağlanıyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "підключається до %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "منسلک ہو رہا ہے %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在连接到 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在連線至 %@" } } } }, "Key" : { "extractionState" : "manual", "localizations" : { "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ключ" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کلید" } } }, "shouldTranslate" : false }, "LuLu must run from within /Applications\r\n" : { "comment" : "LuLu must run from within /Applications\r\n", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu muss aus /Applications heraus ausgeführt werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu debe ejecutarse desde /Applications\n" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu doit être exécuté depuis /Applications" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LuLu deve essere eseguito da /Applications" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu는 /Applications 내에서 실행해야 합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu musi być uruchamiany z katalogu /Applications" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O LuLu deve ser executado a partir de /Applications" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu, /Applications klasöründen çalıştırılmalıdır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu має запускатися з /Applications" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu کو /Applications کے اندر سے چلایا جانا چاہیے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 必须从 /Applications 中运行" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 必須從 /Applications 中運行" } } } }, "LuLu: disabled" : { "comment" : "LuLu: disabled", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: desactivado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu : désactivé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: Disabilitato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: 비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: wyłączona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: desabilitado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: Devre dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: вимкнено" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: غیر فعال" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: 禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu:停用" } } } }, "LuLu: enabled" : { "comment" : "LuLu: enabled", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: activado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu : activé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: Abilitato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: 활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: włączona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: habilitado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: Etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: увімкнено" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: فعال" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu: 启用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu:啟用" } } } }, "LuLu's Network Extension Is Not Running" : { "comment" : "LuLu's Network Extension Is Not Running", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLus Netzwerkerweiterung läuft nicht" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La Extensión de Red de LuLu no está en ejecución" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L’extension réseau de LuLu ne fonctionne pas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "L’estensione di rete di LuLu non è attiva" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu의 네트워크 확장 프로그램이 실행되고 있지 않습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozszerzenie sieciowe LuLu nie działa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A Extensão de Rede do LuLu não está rodando" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu’nun ağ genişletmesi çalışmıyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Мережеве розширення LuLu не працює" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "LuLu کا نیٹ ورک ایکسٹینشن چل رہا نہیں ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 的网络扩展未运行" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LuLu 的網路延伸功能未執行" } } } }, "More Info" : { "comment" : "More Info", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weitere Informationen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Más Información" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Plus d’infos" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Più informazioni" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가 정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Więcej informacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mais Informações" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha Fazla Bilgi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Більше інформації" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مزید معلومات" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更多信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更多資訊" } } } }, "New profile '%@' saved and activated." : { "comment" : "New profile '%@' saved and activated.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neues Profil „%@“ gespeichert und aktiviert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nuevo perfil “%@” guardado y activado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nouveau profil « %@ » enregistré et activé." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nuovo profilo \"%@\" salvato e attivato." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새 프로필 \"%@\"이(가) 저장되고 활성화되었습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nowy profil „%@” został zapisany i aktywowany." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Novo perfil “%@” salvo e ativado." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni “%@” profili kaydedildi ve etkinleştirildi." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Новий профіль «%@» збережено та активовано." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "یا پروفائل '%@' محفوظ کر لیا گیا ہے اور فعال کر دیا گیا ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新配置文件“%@”已保存并激活。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新配置文件”%@”已保存并激活。" } } } }, "Next" : { "comment" : "Next", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weiter" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siguiente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suivant" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avanti" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dalej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Avançar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sonraki" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Далі" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ٹھیک ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "下一步" } } } }, "none" : { "comment" : "none", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "keine" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ninguno" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "aucun" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "nessuno" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nic" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "nenhum(a)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "жоден" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "کوئی نہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無" } } } }, "not applicable" : { "comment" : "not applicable", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "nicht anwendbar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "no aplica" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "non applicable" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "non applicabile" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "해당 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nie dotyczy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "não se aplica" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "uygulanabilir değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "не застосовується" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لاگو نہیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "不适用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "不適用" } } } }, "Note however:\r\n▪ Existing connections will not be impacted.\r\n▪ OS traffic (not routed thru LuLu) will not be blocked." : { "comment" : "Note however:\r\n▪ Existing connections will not be impacted.\r\n▪ OS traffic (not routed thru LuLu) will not be blocked.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beachte:\n▪ Bestehende Verbindungen sind nicht betroffen.\n▪ Betriebssystemverkehr (der nicht über LuLu geleitet wird) wird nicht blockiert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin embargo, ten en cuenta:\n▪ Las conexiones existentes no se verán afectadas.\n▪ El tráfico del sistema operativo (que no es ruteado a través de LuLu) no será bloqueado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "A noter cependant :\n▪ Les connexions existantes ne seront pas affectées.\n▪ Le trafic OS (non acheminé par LuLu) ne sera pas bloqué." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tieni a mente:\n * Le connessioni esistente non verranno impattate.\n * Il traffico di rete del Sistema Operativo (che non passa tramite LuLu) non verrà bloccato." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "참고 사항:\r\n▪ 기존 연결은 영향을 받지 않습니다.\r\n▪ (LuLu를 통하지 않는) OS 트래픽은 차단되지 않습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Należy pamiętać że:\n▪ Istniejące połączenia nie zostaną naruszone.\n▪ Ruch systemu operacyjnego (niekierowany przez LuLu) nie zostanie zablokowany." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Observação:\r\n▪ Conexões existentes não serão impactadas.\r\n▪ Tráfego do sistema operacional (não encaminhado pelo LuLu) não será bloqueado." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ancak şuna dikkat edin:\n ▪ Var olan bağlantılar bundan etkilenmez.\n▪ İşletim sistemi trafiği (LuLu tarafından yönetilmeyen) engellenmez." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зверніть увагу:\n▪ Існуючі з'єднання не будуть порушені. \n▪ Системний трафік, який не проходить через LuLu, не буде заблоковано." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "یہ نوٹ کریں:\r\n▪ موجودہ کنیکشن پر کوئی اثر نہیں پڑے گا۔\r\n▪ او ایس ٹریفک (جو لولو کے ذریعے روٹ نہیں کیا جاتا ہے) کو بلاک نہیں کیا جائے گا۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "请注意:\n▪ 现有连接不会受到影响。\n▪ 操作系统流量(未通过 LuLu 路由)不会被阻止。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "請注意:\n▪ 現有連線不會受影響。\n▪ 作業系統流量(未經過 LuLu)不會被封鎖。" } } } }, "Ok" : { "comment" : "Ok", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aceptar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ok" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tamam" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Гаразд" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ٹھیک ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "确定" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "确定" } } } }, "OK" : { "comment" : "OK", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ok" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tamam" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ОК" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ٹھیک ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "OK" } } } }, "Old Version of LuLu Installed" : { "comment" : "Old Version of LuLu Installed", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Veraltete Version von LuLu installiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión antigua de LuLu instalada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ancienne version de LuLu installée" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "È installata una vecchia versione di LuLu" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이전 버전의 LuLu가 설치됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalowano starą wersję LuLu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão Antiga do LuLu Instalada." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu’nun eski sürümü kurulu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виявлено застарілу версію LuLu" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کا پرانا ورژن انسٹال ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已安装旧版本的 LuLu" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已安裝舊版 LuLu" } } } }, "Only export user created rules" : { "comment" : "Only export user created rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nur benutzererstellte Regeln exportieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Exportar solo las reglas creadas por el usuario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exporter uniquement les règles créées par l'utilisateur" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esporta solo le regole create dall'utente" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자가 생성한 규칙만 내보내기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Eksportuj tylko reguły utworzone przez użytkownika" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exportar apenas regras criadas pelo usuário" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yalnızca kullanıcı tarafından oluşturulan kuralları dışa aktar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Експортувати лише правила, створені користувачем" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "صرف صارف کی بنائی ہوئی قواعد برآمد کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅导出用户创建的规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "僅導出用戶創建的規則" } } } }, "Operating System Programs (required for system functionality)" : { "comment" : "Operating System Programs (required for system functionality)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Betriebssystemprogramme (für Systemfunktionaliät benötigt)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Programas del Sistema Operativo (necesarios para la funcionalidad del sistema)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Programmes du système d'exploitation (nécessaires au fonctionnement du système)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programmi del Sistema Operativo (Richiesti per le funzionalità di sistema)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "운영 체제 프로그램 (시스템 기능에 필요)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Programy systemu operacyjnego (wymagane do prawidłowego działania systemu)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Programas do Sistema Operacional (necessários para funcionalidade do sistema)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşletim sistemi programları (sistem işlevselliği için gerekli)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Системні програми (необхідні для роботи операційної системи)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "آپریٹنگ سسٹم پروگرامز (سسٹم فنکشنیلٹی کے لیے ضروری)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "操作系统程序(系统功能所需)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "作業系統程式(系統功能所需)" } } } }, "Outgoing traffic will now be blocked." : { "comment" : "Outgoing traffic will now be blocked.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgehender Datenverkehr wird jetzt blockiert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tráfico saliente será bloqueado ahora. " } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le trafic sortant est désormais bloqué." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il traffico in uscita non verrà bloccato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이제 나가는 트래픽이 차단됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ruch wychodzący będzie teraz blokowany." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tráfego de saída será bloqueado." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dışarı giden trafik artık engellenecek." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вихідний трафік тепер буде заблоковано." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "آؤٹ گوئنگ ٹریفک اب بلاک کر دی جائے گی۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "传出的流量将被阻止。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "對外流量將被封鎖。" } } } }, "Path(s) for %@:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad/Pfade für %@:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta(s) de %@:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin(s) pour %@ :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso/i per %@:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@의 경로:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka(i) dla %@:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho(s) de %@:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ için yol(lar):" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях(и) для %@:" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "%@ کے لیے راستہ(جات):" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 的路径:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 的路径:" } } } }, "Path(s):" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad/Pfade:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta(s):" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin(s) :" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso/i:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka(i):" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho(s):" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yol(lar):" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях(и):" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "راستہ(جات):" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "路径:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "路径:" } } } }, "Pre-installed 3rd-party Programs (automatically allowed & added here if 'allow installed applications' is set)" : { "comment" : "Pre-installed 3rd-party Programs (automatically allowed & added here if 'allow installed applications' is set)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vorinstallierte Drittanbieterprogramme (automatisch zugelassen und hier hinzugefügt, wenn „Bereits installierte Anwendungen erlauben“ aktiviert ist)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Programas de terceros preinstalados (permitidos automáticamente y agregados aquí si está configurado 'permitir aplicaciones instaladas')" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Programmes tiers préinstallés (automatiquement autorisés et ajoutés ici si l'option « autoriser les applications installées » est activée)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programmi di terze parti pre-installati (approvati automaticamente e mostrati qui se “Abilita applicazioni installate” è attivo)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사전에 설치한 타사 프로그램 ('설치된 프로그램 허용' 설정에 따라 자동으로 허용 및 추가됨)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wstępnie zainstalowane programy innych firm (automatycznie dozwolone i dodawane tutaj, jeśli ustawiono opcję „zezwalaj na zainstalowane aplikacje”)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Programas Terceirizados Pré-Instalados (automaticamente permitidos e adicionados aqui se ‘permitir aplicações instaladas’ estiver selecionado)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önceden kurulu 3. parti uygulamalar (“Kurulu uygulamalara izin ver” ayarlıysa kendiliğinden eklenir ve izin verilir)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Попередньо встановлені сторонні програми (автоматично дозволяються й додаються сюди, якщо увімкнено «Дозволити встановлені програми»)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پہلے سے انسٹال شدہ تھرڈ پارٹی پروگرامز (اگر 'انسٹال شدہ ایپلیکیشنز کو اجازت دیں' سیٹ ہو تو خودکار طور پر اجازت دی جاتی ہے اور یہاں شامل کی جاتی ہے)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已安装的第三方程序(如果设置了“允许已安装的应用程序”,则自动允许并添加到此处)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已安裝的第三方程式(若啟用「允許已安裝的應用程式」,則會自動允許並新增至此)" } } } }, "Process Arguments: %@" : { "comment" : "Process Arguments: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozessargumente: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Argumentos Procesados: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Arguments de processus : %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Argomenti del processo: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스 인수: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Argumenty procesu: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Argumentos do Processo: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem argümanları: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Параметри процесу: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروسیس آرگومنٹس: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进程参数:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Process 參數:%@" } } } }, "Process Path: %@" : { "comment" : "Process Path: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozesspfad: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta del Proceso: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin du processus : %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso del processo: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로세스 경로: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka procesu: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho do Processo: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem yolu: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях процесу: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروسیس پاتھ: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进程路径:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Process 路徑:%@" } } } }, "Profile name can't be blank" : { "comment" : "Profile name can't be blank", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profilname darf nicht leer sein" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El nombre del perfil no puede estar vacío" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le nom du profil ne peut pas être vide" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il nome del profilo non può essere vuoto" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 이름은 비워둘 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa profilu nie może być pusta" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O nome do perfil não pode estar vazio" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil adı boş bırakılamaz" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва профілю не може бути порожньою" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل کا نام خالی نہیں ہو سکتا" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名称不能为空" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "配置文件名稱不能為空" } } } }, "Profile Switched" : { "comment" : "Profile Switched", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil gewechselt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil cambiado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil changé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo cambiato" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필 전환됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profil przełączony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil alterado" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil Değiştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профіль перемкнено" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل تبدیل کر دیا گیا ہے" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件已切换" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "配置文件已切换" } } } }, "Profile: %@" : { "comment" : "Profile: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil : %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profil: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профіль: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروفائل: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "配置文件:%@" } } } }, "Profile: Default" : { "comment" : "Profile: Default", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Profil: Default" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Perfil: Default" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profil : Default" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profilo: Default" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로필: Default" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Profil: Default" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Perfil: Default" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Profil: Saptanmış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Профіль: Default" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "روفائل: Default" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置文件:Default" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "配置文件:Default" } } } }, "Programs within \"%@/\"" : { "comment" : "Programs within \"%@/\"", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programme in \"%@/\"" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Programas dentro de \"%@/\"" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Programmes dans \"%@/\"" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programmi all'interno di \"%@/\"" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "\"%@/\" 내 프로그램" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Programy w \"%@/\"" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Programas em “%@/“" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "“%@/“ içindeki uygulamalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програми в \"%@/\"" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "پروگرامز اندر \"%@/\"" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "“%@/”内的程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "\"%@/\" 中的程式" } } } }, "Quit" : { "comment" : "Quit", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Termina" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "종료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyjdź" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sair" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завершити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ختم کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉" } } } }, "Quit LuLu?" : { "comment" : "Quit LuLu?", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu beenden?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar LuLu?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter LuLu ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Terminare LuLu?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu를 종료할까요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyjść z LuLu?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sair do LuLu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu’dan çıkılsın mı?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завершити роботу з LuLu?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کو ختم کریں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭LuLu ?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉 LuLu?" } } } }, "Reverse Domain: %@" : { "comment" : "Reverse Domain %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Reverse Domain: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dominio Inverso: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Domaine inversé : %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dominio inverso: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "리버스 도메인: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reverse Domain: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Domínio Reverso: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ters etki alanı: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зворотний домен: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ریورس ڈومین: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "反向域 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "反向網域:%@" } } } }, "Rule" : { "comment" : "Rule", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regel" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Regla" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règle" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regola" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguła" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regra" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kural" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правило" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اصول" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "规则" } } } }, "Rules" : { "comment" : "Rules", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regeln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reglas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Règles" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regole" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "규칙들" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reguły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regras" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurallar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правила" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اصول" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "规则" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "规则" } } } }, "rules.json" : { "comment" : "rules.json", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "rules.json" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "reglas.json" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "rules.json" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "rules.json" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "rules.json" } } }, "shouldTranslate" : false }, "See log for (more) details" : { "comment" : "See log for (more) details", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prüfe das Log für (weitere) Details" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Consulta el registro para más detalles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir le journal pour (plus) de détails" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi i log per altre informazioni" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "더 자세한 사항은 로그를 참조하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz dziennik, aby uzyskać (więcej) szczegółów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Veja registro para (mais) detalhes" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıntılar için günlüğe bakın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дивіться лог для детальнішої інформації" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "مزید تفصیلات کے لیے لاگ دیکھیں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看日志了解更多详细信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "請查看日誌以獲取(更多)詳細資訊" } } } }, "This must be uninstalled before continuing. Click 'More Info' to learn how to uninstall it" : { "comment" : "This must be uninstalled before continuing\r\n.Click 'More Info' to learn how to uninstall it.", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies muss zuerst deinstalliert werden.\r\nKlicke auf „Weitere Informationen“, um zu erfahren, wie du es deinstallieren kannst." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto debe desinstalarse antes de continuar. Haz click en 'Más información' para saber cómo desinstalarlo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ceci doit être désinstallé avant de continuer. Cliquez sur « Plus d'informations » pour savoir comment le désinstaller." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prima di continuare, il programma deve essere disinstallato. Clicca “Più informazioni” per scoprire come" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "계속하기 전 반드시 삭제해야 합니다. '추가 정보'를 클릭하여 삭제 방법을 알 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Należy odinstalować przed kontynuowaniem. Kliknij „Więcej informacji”, aby dowiedzieć się, jak odinstalować" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isto deve ser desinstalado antes de continuar. Clique em ‘Mais Informações’ para aprender como desinstalar." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürdürme öncesi bunun kaldırılması gerekiyor. Nasıl kaldıracağınızı öğrenmek için “Daha Fazla Bilgi”ye tıklayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Щоб продовжити, необхідно це видалити. Натисніть «Більше інформації», щоб дізнатися, як це видалити." } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اسے جاری رکھنے سے پہلے ان انسٹال کرنا ہوگا۔ مزید معلومات کے لیے 'مزید معلومات' پر کلک کریں۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "必须先卸载才能继续。点击“更多信息”了解如何卸载它。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "必須先解除安裝才能繼續。請點擊「更多資訊」了解如何解除安裝。" } } } }, "Time stamp: %@" : { "comment" : "Time stamp: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitstempel: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Marca de tiempo: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Horodatage : %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Marca temporale: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "타임 스탬프: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sygnatura czasowa: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Registro da hora: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Zaman damgası: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Мітка часу: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ٹائم اسٹیمپ: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "时间戳:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "時間戳:%@" } } } }, "Uninstall" : { "comment" : "Uninstall", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deinstallieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "انسٹال کریں" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝" } } } }, "Uninstall LuLu?" : { "comment" : "Uninstall LuLu?", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LuLu deinstallieren?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar LuLu?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller LuLu ?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla LuLu?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LuLu를 삭제할까요?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstalować LuLu?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar LuLu?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LuLu kaldırılsın mı?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити LuLu?" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "لولو کو ان انسٹال کریں؟" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载LuLu ?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝 LuLu?" } } } }, "unknown" : { "comment" : "unknown", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "unbekannt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "desconocido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "inconnu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "sconosciuto" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "알 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "nieznany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desconhecido" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "bilinmiyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "невідомий" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نامعلوم" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未知" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未知" } } } }, "Update" : { "comment" : "Update", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اپ ڈیٹ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新" } } } }, "Update available, but isn't supported on macOS %ld.%ld" : { "comment" : "Update available, but isn't supported on macOS %ld.%ld", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update verfügbar, wird jedoch auf macOS %ld.%ld nicht unterstützt." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "Update available, but isn't supported on macOS %1$ld.%2$ld" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualización disponible, pero no es compatible con macOS %ld.%ld" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour disponible, mais non prise en charge sur macOS %ld.%ld" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento disponibile, ma non supportato su macOS %ld.%ld" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트를 사용할 수 있으나, macOS %ld.%ld에서는 지원되지 않습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja jest dostępna, ale nie jest obsługiwana w macOS %ld.%ld." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização disponível, mas não é compatível com o macOS %ld.%ld" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme kullanılabilir; ancak macOS %ld.%ld üzerinde desteklenmiyor." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доступне оновлення, але воно не підтримується в macOS %ld.%ld" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "اپ ڈیٹ دستیاب ہے، لیکن macOS %1$ld.%2$ld پر تعاون یافتہ نہیں ہے۔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "有可用更新,但不支持 macOS %ld.%ld" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "有可用更新,但不支持在 macOS %ld.%ld 上运行" } } } }, "User-specified Programs (manually added, or in response to an alert)" : { "comment" : "User-specified Programs (manually added, or in response to an alert)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vom Benutzer festgelegte Programme (manuell hinzugefügt oder als Reaktion auf eine Mitteilung)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Programas especificados por el usuario (agregados manualmente, o en respuesta a una alerta)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Programmes spécifiés par l'utilisateur (ajoutés manuellement ou en réponse à une alerte)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programmi specificati dall'utente (aggiunti manualmente o in risposta a un avviso)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자 지정 프로그램 (수동 또는 경고 창에 의해 추가됨)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Programy określone przez użytkownika (dodane ręcznie lub w odpowiedzi na alert)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Programas especificados pelo usuário (adicionados manualmente, ou em resposta à um alerta)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kullanıcı tanımlı programlar (elle veya bir ikâza yönelik eklenir)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програми, вказані користувачем (додані вручну або у відповідь на сповіщення)" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "صارف کے ذریعہ مخصوص پروگرامز (مینیول طور پر شامل کیے گئے ہیں، یا انتباہ کے جواب میں)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "用户指定的程序(手动添加或响应警报)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "使用者指定的程式(手動新增或因警告而加入)" } } } }, "Version: %@" : { "comment" : "Version: %@", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Version : :@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Версія: %@" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "ورژن: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "版本: %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版本:%@" } } } }, "Waiting for Network Extension Approval" : { "comment" : "Waiting for Network Extension Approval", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Warten auf Genehmigung der Netzwerkerweiterung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esperando la aprobación de la Extensión de Red" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En attente de l'approbation de l'extension réseau" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In attesa di approvazione per l’estensione di rete" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "네트워크 확장 프로그램 승인 대기 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Oczekiwanie na zatwierdzenie rozszerzenia sieci" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aguardando aprovação da Extensão de Rede" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ağ genişletmesi onayı bekleniyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очікується схвалення мережевого розширення" } }, "ur" : { "stringUnit" : { "state" : "translated", "value" : "نیٹ ورک ایکسٹینشن کی منظوری کا انتظار" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "等待网络扩展批准" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在等待網路延伸功能被核准" } } } } }, "version" : "1.0" } ================================================ FILE: LuLu/Shared/Rule.h ================================================ // // file: Rule.h // project: LuLu (shared) // description: Rule object (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #ifndef Rule_h #define Rule_h @import OSLog; @import Foundation; @interface Rule : NSObject { } /* PROPERTIES */ //uuid @property(nonatomic, retain)NSString* uuid; //key @property(nonatomic, retain)NSString* key; // PROCESS/BINARY INFO //rule pid // only set if rule's duration is set to process lifetime @property(nonatomic, retain)NSNumber* pid; //path @property(nonatomic, retain)NSString* path; //flag for global rule @property(nonatomic, retain)NSNumber* isGlobal; //flag for directory rule @property(nonatomic, retain)NSNumber* isDirectory; //name @property(nonatomic, retain)NSString* name; //signing info @property(nonatomic, retain)NSDictionary* csInfo; //remote ip or url @property(nonatomic, retain)NSString* endpointAddr; //remote host @property(nonatomic, retain)NSString* endpointHost; //flag for endpoint addr @property BOOL isEndpointAddrRegex; //remote port @property(nonatomic, retain)NSString* endpointPort; //type // default, user, etc @property(nonatomic, retain)NSNumber* type; //protocol @property(nonatomic, retain)NSNumber* protocol; //is disabled @property(nonatomic, retain)NSNumber* isDisabled; // TIMESTAMPS //rule creation @property(nonatomic, retain)NSDate* creation; //rule expiration // only set if rule's duration is set to expire @property(nonatomic, retain)NSDate* expiration; // ACTION //action // allow / deny @property(nonatomic, retain)NSNumber* action; //action scope // process, endpoint, etc @property(nonatomic, retain)NSNumber* scope; /* METHODS */ //init method -(id)init:(NSDictionary*)info; //matches a string? -(BOOL)matchesString:(NSString*)match; //matches a(nother) rule? -(BOOL)isEqualToRule:(Rule *)rule; //is rule temp? -(BOOL)isTemporary; //is rule user (created) -(BOOL)isUserCreated; //covert to dictionary -(NSMutableString*)toJSON; //make a rule obj from a dictioanary -(id)initFromJSON:(NSDictionary*)info; @end #endif /* Rule_h */ ================================================ FILE: LuLu/Shared/Rule.m ================================================ // // file: Rule.h // project: LuLu (shared) // description: Rule object (header) // // created by Patrick Wardle // copyright (c) 2020 Objective-See. All rights reserved. // #import "Rule.h" #import "consts.h" #import "utilities.h" #import /* GLOBALS */ //log handle extern os_log_t logHandle; @implementation Rule @synthesize scope; @synthesize action; //init method -(id)init:(NSDictionary*)info { //init super if(self = [super init]) { //url NSURL* remoteURL = nil; //dbg msg os_log_debug(logHandle, "creating rule with: %{public}@", info); //create UUID self.uuid = [[NSUUID UUID] UUIDString]; //init pid // note: only set for temporary rules if(YES == [info[KEY_DURATION_PROCESS] boolValue]) { //set self.pid = info[KEY_PROCESS_ID]; } //set creation self.creation = [NSDate date]; //init expiration // though this won't (usually) be set self.expiration = info[KEY_DURATION_EXPIRATION]; //init path self.path = info[KEY_PATH]; //init name self.name = (nil != info[KEY_PROCESS_NAME]) ? info[KEY_PROCESS_NAME] : getProcessName(0, self.path); //init signing info self.csInfo = info[KEY_CS_INFO]; //process scope (set via alert) // set endpoint info to all ('*') if( (nil != info[KEY_SCOPE]) && (ACTION_SCOPE_PROCESS == [info[KEY_SCOPE] intValue]) ) { //dbg msg os_log_debug(logHandle, "rule info has 'KEY_SCOPE' set to 'ACTION_SCOPE_PROCESS'"); //any addr self.endpointAddr = VALUE_ANY; //any port self.endpointPort = VALUE_ANY; } //other use endpoint info // or if nil, set to all ('*') else { //init addr // nil? default to all ('*') self.endpointAddr = (nil != info[KEY_ENDPOINT_ADDR]) ? info[KEY_ENDPOINT_ADDR] : VALUE_ANY; //is endpoint addr a regex? self.isEndpointAddrRegex = [info[KEY_ENDPOINT_ADDR_IS_REGEX] boolValue]; //init port // nil? default to all ('*') self.endpointPort = (nil != info[KEY_ENDPOINT_PORT]) ? info[KEY_ENDPOINT_PORT] : VALUE_ANY; } //init URL obj (w/ scheme) // so we can extract a host if(YES != [self.endpointAddr isEqualToString:VALUE_ANY]) { //init url w/ scheme if(YES != [self.endpointAddr hasPrefix:@"http"]) { //init url remoteURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", self.endpointAddr]]; } //no scheme needed else { //init url remoteURL = [NSURL URLWithString:self.endpointAddr]; } //now with URL obj, get host name self.endpointHost = remoteURL.host; } //set proto self.protocol = info[KEY_PROTOCOL]; //set type self.type = info[KEY_TYPE]; //init action self.action = info[KEY_ACTION]; //now, generate key if(nil != info[KEY_KEY]) { //set self.key = info[KEY_KEY]; } //generate key else { //generate self.key = [self generateKey]; } } return self; } //generate key // note: this matches process' generate key algo -(NSString*)generateKey { //id NSString* key = nil; //signer NSInteger signer = None; //cs info? if(nil != self.csInfo) { //extract signer signer = [self.csInfo[KEY_CS_SIGNER] intValue]; //apple/app store // just use cs id if( (Apple == signer) || (AppStore == signer) ) { //set key key = self.csInfo[KEY_CS_ID]; } //dev id? // use cs id + (leaf) signer else if(DevID == signer) { //check for cs id/auths if( (0 != [self.csInfo[KEY_CS_ID] length]) && (0 != [self.csInfo[KEY_CS_AUTHS] count]) ) { //set key = [NSString stringWithFormat:@"%@:%@", self.csInfo[KEY_CS_ID], [self.csInfo[KEY_CS_AUTHS] firstObject]]; } } } //no valid cs info, etc // just use item's path if(0 == key.length) { //set key = self.path; } //dbg msg os_log_debug(logHandle, "generated rule key: %{public}@", key); return key; } //is rule global? -(NSNumber*)isGlobal { //first time? // init and set if(nil == _isGlobal) { //set _isGlobal = [NSNumber numberWithBool:[self.path isEqualToString:VALUE_ANY]]; } return _isGlobal; } //is rule temporary? // ...just if its duration is set to process lifetime (e.g. has a pid) -(BOOL)isTemporary { return (nil != self.pid); } //is rule user (created)? -(BOOL)isUserCreated { return (self.type.intValue == RULE_TYPE_USER); } //is rule directory? -(NSNumber*)isDirectory { //first time? // init and set if(nil == _isDirectory) { //set _isDirectory = [NSNumber numberWithBool:((YES == [self.path hasPrefix:@"/"]) && (YES == [self.path hasSuffix:@"/*"]))]; } return _isDirectory; } //required as we support secure coding +(BOOL)supportsSecureCoding { return YES; } //init with coder -(id)initWithCoder:(NSCoder *)decoder { //super if(self = [super init]) { //decode rule object self.key = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(key))]; self.uuid = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(uuid))]; self.pid = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(pid))]; self.path = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(path))]; self.name = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(name))]; self.csInfo = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[[NSDictionary class], [NSArray class], [NSString class], [NSNumber class]]] forKey:NSStringFromSelector(@selector(csInfo))]; self.isEndpointAddrRegex = [decoder decodeBoolForKey:NSStringFromSelector(@selector(isEndpointAddrRegex))]; self.endpointAddr = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(endpointAddr))]; self.endpointHost = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(endpointHost))]; self.endpointPort = [decoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(endpointPort))]; self.type = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(type))]; self.scope = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(scope))]; self.action = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(action))]; self.isDisabled = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(isDisabled))]; self.creation = [decoder decodeObjectOfClass:[NSDate class] forKey:NSStringFromSelector(@selector(creation))]; self.expiration = [decoder decodeObjectOfClass:[NSDate class] forKey:NSStringFromSelector(@selector(expiration))]; } return self; } //encode with coder -(void)encodeWithCoder:(NSCoder *)encoder { //encode rule object [encoder encodeObject:self.key forKey:NSStringFromSelector(@selector(key))]; [encoder encodeObject:self.uuid forKey:NSStringFromSelector(@selector(uuid))]; [encoder encodeObject:self.pid forKey:NSStringFromSelector(@selector(pid))]; [encoder encodeObject:self.path forKey:NSStringFromSelector(@selector(path))]; [encoder encodeObject:self.name forKey:NSStringFromSelector(@selector(name))]; [encoder encodeObject:self.csInfo forKey:NSStringFromSelector(@selector(csInfo))]; [encoder encodeObject:self.endpointAddr forKey:NSStringFromSelector(@selector(endpointAddr))]; [encoder encodeObject:self.endpointHost forKey:NSStringFromSelector(@selector(endpointHost))]; [encoder encodeObject:self.endpointPort forKey:NSStringFromSelector(@selector(endpointPort))]; [encoder encodeBool:self.isEndpointAddrRegex forKey:NSStringFromSelector(@selector(isEndpointAddrRegex))]; [encoder encodeObject:self.type forKey:NSStringFromSelector(@selector(type))]; [encoder encodeObject:self.scope forKey:NSStringFromSelector(@selector(scope))]; [encoder encodeObject:self.action forKey:NSStringFromSelector(@selector(action))]; [encoder encodeObject:self.isDisabled forKey:NSStringFromSelector(@selector(isDisabled))]; [encoder encodeObject:self.creation forKey:NSStringFromSelector(@selector(creation))]; [encoder encodeObject:self.expiration forKey:NSStringFromSelector(@selector(expiration))]; return; } //matches a string? // used for filtering in UI -(BOOL)matchesString:(NSString*)match { //match BOOL matches = NO; //rule action (as string) NSString* action = nil; //init w/ allow if(RULE_STATE_ALLOW == self.action.integerValue) { action = NSLocalizedString(@"Allow", "@Allow"); } //init w/ block else if(RULE_STATE_BLOCK == self.action.integerValue) { action = NSLocalizedString(@"Block", @"Block"); } //check name, path if( (YES == [self.name localizedCaseInsensitiveContainsString:match]) || (YES == [self.path localizedCaseInsensitiveContainsString:match]) ) { //match matches = YES; goto bail; } //check pid if( (nil != self.pid) && (YES == [self.pid.stringValue containsString:match]) ) { //match matches = YES; goto bail; } //check cs id if( (nil != self.csInfo[KEY_CS_ID]) && (YES == [self.csInfo[KEY_CS_ID] localizedCaseInsensitiveContainsString:match]) ) { //match matches = YES; goto bail; } //endpoint addr/port if( (YES == [self.endpointAddr localizedCaseInsensitiveContainsString:match]) || (YES == [self.endpointPort localizedCaseInsensitiveContainsString:match]) ) { //match matches = YES; goto bail; } //endpoint addr ('any') if( (YES == [self.endpointAddr isEqualToString:VALUE_ANY]) && (YES == [match isEqualToString:NSLocalizedString(@"any address", @"any address")]) ) { //match matches = YES; goto bail; } //endpoint port ('any') if( (YES == [self.endpointPort isEqualToString:VALUE_ANY]) && (YES == [match isEqualToString:NSLocalizedString(@"any port", @"any port")]) ) { //match matches = YES; goto bail; } //check state if( (nil != action) && (YES == [action localizedCaseInsensitiveContainsString:match]) ) { //match matches = YES; goto bail; } bail: return matches; } //matches a(nother) rule? -(BOOL)isEqualToRule:(Rule *)rule { return [self.uuid isEqualToString:rule.uuid]; } //override description method // allows rules to be 'pretty-printed' -(NSString*)description { id pid = @"all"; id isDisabled = @NO; id expiration = @"never"; //has pid? if(self.pid) { pid = self.pid; } //has expiration? if(self.expiration) { expiration = self.expiration; } //disabled? if(self.isDisabled) { isDisabled = self.isDisabled; } //just serialize return [NSString stringWithFormat:@"RULE: pid: %@, path: %@, name: %@, endpoint addr: %@, endpoint port: %@, action: %@, type: %@, disabled: %@, creation: %@, expiration: %@", pid, self.path, self.name, self.endpointAddr, self.endpointPort, self.action, self.type, isDisabled, self.creation, expiration]; } //covert rule to dictionary // needed for conversion to JSON // note: temporary properties (such as pid) not included -(NSMutableString*)toJSON { //json NSMutableString* json = nil; //escaped NSString* escaped = nil; //date formatter NSDateFormatter* dateFormatter = nil; //init formatter // format: ISO 8601 format dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; //init json = [NSMutableString string]; //key [json appendFormat:@"\"%@\" : \"%@\",", NSStringFromSelector(@selector(key)), self.key]; //uuid [json appendFormat:@"\"%@\" : \"%@\",", NSStringFromSelector(@selector(uuid)), self.uuid]; //path escaped = toEscapedJSON(self.path); if(nil != escaped) { [json appendFormat:@"\"%@\" : %@,", NSStringFromSelector(@selector(path)), escaped]; } //name escaped = toEscapedJSON(self.name); if(nil != escaped) { [json appendFormat:@"\"%@\" : %@,", NSStringFromSelector(@selector(name)), escaped]; } escaped = toEscapedJSON(self.endpointAddr); if(nil != escaped) { [json appendFormat:@"\"%@\" : %@,", NSStringFromSelector(@selector(endpointAddr)), escaped]; } if(nil != self.endpointHost) { escaped = toEscapedJSON(self.endpointHost); if(nil != escaped) { [json appendFormat:@"\"%@\" : %@,", NSStringFromSelector(@selector(endpointHost)), escaped]; } } //creation if(nil != self.creation) { [json appendFormat:@"\"%@\" : \"%@\",", NSStringFromSelector(@selector(creation)), [dateFormatter stringFromDate:self.creation]]; } //expiration if(nil != self.expiration) { [json appendFormat:@"\"%@\" : \"%@\",", NSStringFromSelector(@selector(expiration)), [dateFormatter stringFromDate:self.expiration]]; } //port [json appendFormat:@"\"%@\" : \"%@\",", NSStringFromSelector(@selector(endpointPort)), self.endpointPort]; //is regex [json appendFormat:@"\"%@\" : %d,", NSStringFromSelector(@selector(isEndpointAddrRegex)), self.isEndpointAddrRegex]; //type [json appendFormat:@"\"%@\" : %d,", NSStringFromSelector(@selector(type)), self.type.intValue]; //disabled if(nil != self.isDisabled) { [json appendFormat:@"\"%@\" : %d,", NSStringFromSelector(@selector(isDisabled)), self.isDisabled.intValue]; } //scope [json appendFormat:@"\"%@\" : %d,", NSStringFromSelector(@selector(scope)), self.scope.intValue]; //action [json appendFormat:@"\"%@\" : %d,", NSStringFromSelector(@selector(action)), self.action.intValue]; //cs info // dictionary... if(nil != self.csInfo) { [json appendFormat:@"\"%@\" : {", NSStringFromSelector(@selector(csInfo))]; //convert each key/value pair for(NSString* key in self.csInfo) { //extract value id value = self.csInfo[key]; //string? if(YES == [value isKindOfClass:[NSString class]]) { escaped = toEscapedJSON(value); if(nil != escaped) { //append [json appendFormat:@"\"%@\" : %@,", key, escaped]; } } //number? if(YES == [value isKindOfClass:[NSNumber class]]) { //append [json appendFormat:@"\"%@\" : %d,", key, [value intValue]]; } //array? if(YES == [value isKindOfClass:[NSArray class]]) { //append [json appendFormat:@"\"%@\" : [", key]; //add each item for(id item in value) { //string? if(YES == [item isKindOfClass:[NSString class]]) { escaped = toEscapedJSON(item); if(nil != escaped) { //append [json appendFormat:@"%@,", escaped]; } } else { [json appendFormat:@"\"%@\",", item]; } } //remove last ',' if(YES == [json hasSuffix:@","]) { //remove [json deleteCharactersInRange:NSMakeRange(json.length-1, 1)]; } //end [json appendFormat:@"],"]; } } //remove last ',' if(YES == [json hasSuffix:@","]) { //remove [json deleteCharactersInRange:NSMakeRange(json.length-1, 1)]; } //end [json appendFormat:@"}"]; } //remove last ',' if(YES == [json hasSuffix:@","]) { //remove [json deleteCharactersInRange:NSMakeRange(json.length-1, 1)]; } return json; } //make a rule obj from a dictioanary -(id)initFromJSON:(NSDictionary*)info { id value = nil; //date formatter NSDateFormatter* dateFormatter = nil; //init formatter // format: ISO 8601 format dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; //dbg msg //os_log_debug(logHandle, "method '%s' invoked", __PRETTY_FUNCTION__); //super if(self = [super init]) { //init + sanity checks self.key = info[NSStringFromSelector(@selector(key))]; if(YES != [self.key isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'key' should be a string, not %@", [self.key class]); self = nil; goto bail; } self.uuid = info[NSStringFromSelector(@selector(uuid))]; if(YES != [self.uuid isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'uuid' should be a string, not %@", [self.uuid class]); self = nil; goto bail; } self.path = info[NSStringFromSelector(@selector(path))]; if(YES != [self.path isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'path' should be a string, not %@", [self.path class]); self = nil; goto bail; } self.name = info[NSStringFromSelector(@selector(name))]; if(YES != [self.name isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'name' should be a string, not %@", [self.name class]); self = nil; goto bail; } self.csInfo = info[NSStringFromSelector(@selector(csInfo))]; if( (nil != self.csInfo) && (YES != [self.csInfo isKindOfClass:[NSDictionary class]]) ) { //err msg os_log_error(logHandle, "ERROR: 'csInfo' should be a dictionary, not %@", [self.csInfo class]); self = nil; goto bail; } self.endpointAddr = info[NSStringFromSelector(@selector(endpointAddr))]; if(YES != [self.endpointAddr isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'endpointAddr' should be a string, not %@", [self.endpointAddr class]); self = nil; goto bail; } self.endpointHost = info[NSStringFromSelector(@selector(endpointHost))]; if( (nil != self.endpointHost) && (YES != [self.endpointHost isKindOfClass:[NSString class]]) ) { //err msg os_log_error(logHandle, "ERROR: 'endpointHost' should be a string, not %@", [self.endpointHost class]); self = nil; goto bail; } self.endpointPort = info[NSStringFromSelector(@selector(endpointPort))]; if(YES != [self.endpointPort isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'endpointPort' should be a string, not %@", [self.endpointPort class]); self = nil; goto bail; } self.isEndpointAddrRegex = [info[NSStringFromSelector(@selector(isEndpointAddrRegex))] boolValue]; self.type = info[NSStringFromSelector(@selector(type))]; if([self.type isKindOfClass:[NSString class]]) { self.type = @([(NSString*)self.type integerValue]); } if(YES != [self.type isKindOfClass:[NSNumber class]]) { //err msg os_log_error(logHandle, "ERROR: 'type' should be a number, not %@", [self.type class]); self = nil; goto bail; } self.scope = info[NSStringFromSelector(@selector(scope))]; if([self.scope isKindOfClass:[NSString class]]) { self.scope = @([(NSString*)self.scope integerValue]); } if(YES != [self.scope isKindOfClass:[NSNumber class]]) { //err msg os_log_error(logHandle, "ERROR: 'scope' should be a number, not %@", [self.scope class]); self = nil; goto bail; } self.action = info[NSStringFromSelector(@selector(action))]; if([self.action isKindOfClass:[NSString class]]) { self.action = @([(NSString*)self.action integerValue]); } if(YES != [self.action isKindOfClass:[NSNumber class]]) { //err msg os_log_error(logHandle, "ERROR: 'action' should be a number, not %@", [self.action class]); self = nil; goto bail; } //disabled? // note: optional self.isDisabled = info[NSStringFromSelector(@selector(isDisabled))]; if(self.isDisabled && [self.isDisabled isKindOfClass:[NSString class]]) { self.isDisabled = @([(NSString*)self.isDisabled integerValue]); } if(self.isDisabled && ![self.isDisabled isKindOfClass:[NSNumber class]]) { //err msg os_log_error(logHandle, "ERROR: 'disabled' should be a number or nil, not %@", [self.isDisabled class]); self = nil; goto bail; } //creation (date) value = info[NSStringFromSelector(@selector(creation))]; if(value) { if(![value isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'creation' should be a string, not %@", [value class]); self = nil; goto bail; } self.creation = [dateFormatter dateFromString:value]; if(!self.creation) { //err msg os_log_error(logHandle, "ERROR: 'creation' date string is invalid: %@", value); self = nil; goto bail; } } //expiration value = info[NSStringFromSelector(@selector(expiration))]; if(value) { if(![value isKindOfClass:[NSString class]]) { //err msg os_log_error(logHandle, "ERROR: 'expiration' should be a string, not %@", [value class]); self = nil; goto bail; } self.expiration = [dateFormatter dateFromString:value]; if(!self.expiration) { //err msg os_log_error(logHandle, "ERROR: 'expiration' date string is invalid: %@", value); self = nil; goto bail; } } } bail: return self; } @end ================================================ FILE: LuLu/Shared/XPCDaemonProto.h ================================================ // // file: XPCDaemonProtocol.h // project: LuLu (shared) // description: methods exported by the daemon // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // @import Foundation; @protocol XPCDaemonProtocol //get preferences -(void)getPreferences:(void (^)(NSDictionary*))reply; //update preferences -(void)updatePreferences:(NSDictionary*)preferences reply:(void (^)(NSDictionary*))reply; //get rules -(void)getRules:(void (^)(NSData*))reply; //add rule -(void)addRule:(NSDictionary*)info; //disable (or re-enable) rule -(void)toggleRule:(NSString*)key rule:(NSString*)uuid state:(NSNumber*)state; //delete rule -(void)deleteRule:(NSString*)key rule:(NSString*)uuid; //import rules -(void)importRules:(NSData*)newRules userOnly:(BOOL)userOnly result:(void (^)(BOOL))reply; //cleanup rules -(void)cleanupRules:(BOOL)fule reply:(void (^)(NSInteger))reply; //get current profile -(void)getCurrentProfile:(void (^)(NSString*))profile; //get list of profiles -(void)getProfiles:(void (^)(NSArray*))reply; //add profile -(void)addProfile:(NSString*)name preferences:(NSDictionary*)preferences reply:(void (^)(BOOL))reply; //delete profile -(void)deleteProfile:(NSString*)name reply:(void (^)(BOOL))reply; //set profile -(void)setProfile:(NSString*)name reply:(void (^)(BOOL))reply; //uninstall -(void)uninstall:(void (^)(BOOL))reply; @end ================================================ FILE: LuLu/Shared/XPCUserProto.h ================================================ // // file: XPCUserProtocol // project: LuLu (shared) // description: protocol for talking to the user (header) // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // @import Foundation; @protocol XPCUserProtocol //rules changed -(void)rulesChanged; //show an alert -(void)alertShow:(NSDictionary*)alert reply:(void (^)(NSDictionary*))reply; @end ================================================ FILE: LuLu/Shared/consts.h ================================================ // // file: consts.h // project: lulu (shared) // description: #defines and what not // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #ifndef consts_h #define consts_h //signers enum Signer{None, Apple, AppStore, DevID, AdHoc}; //patreon url #define PATREON_URL @"https://www.patreon.com/join/objective_see" //vendor id string #define OBJECTIVE_SEE_VENDOR "com.objectiveSee" //bundle ID #define BUNDLE_ID "com.objective-see.lulu" //extension bundle ID #define EXT_BUNDLE_ID @"com.objective-see.lulu.extension" //main app bundle id #define APP_ID @"com.objective-see.lulu.app" //signing auth #define SIGNING_AUTH @"Developer ID Application: Objective-See, LLC (VBG97UB4TA)" //firewall event: new flow #define LULU_EVENT @"com.objective-see.lulu.event" //lulu service #define LULU_SERVICE_NAME "com_objective_see_firewall" //install directory #define INSTALL_DIRECTORY @"/Library/Objective-See/LuLu" //preferences file #define PREFS_FILE @"preferences.plist" //rules file #define RULES_FILE @"rules.plist" //(old) rules file #define RULES_FILE_V1 @"rules_v1.plist" //client no status #define STATUS_CLIENT_UNKNOWN -1 //client disabled #define STATUS_CLIENT_DISABLED 0 //client enabled #define STATUS_CLIENT_ENABLED 1 //daemon mach name #define DAEMON_MACH_SERVICE @"VBG97UB4TA.com.objective-see.lulu" //rule state; not found #define RULE_STATE_NOT_FOUND -1 //rule state; block #define RULE_STATE_BLOCK 0 //rule state; allow #define RULE_STATE_ALLOW 1 //product version url #define PRODUCT_VERSIONS_URL @"https://objective-see.org/products.json" //app name #define PRODUCT_NAME @"LuLu" //product url #define PRODUCT_URL @"https://objective-see.org/products/lulu.html" //error(s) url #define ERRORS_URL @"https://objective-see.org/errors.html" //support us button tag #define BUTTON_SUPPORT_US 100 //more info button tag #define BUTTON_MORE_INFO 101 //install cmd #define CMD_INSTALL @"-install" //uninstall cmd #define CMD_UNINSTALL @"-uninstall" //flag to uninstall #define ACTION_UNINSTALL_FLAG 0 //flag to install #define ACTION_INSTALL_FLAG 1 //flag for partial uninstall // leave preferences file, etc. #define UNINSTALL_PARTIAL 0 //flag for full uninstall #define UNINSTALL_FULL 1 //add rule, block #define BUTTON_BLOCK 0 //add rule, allow #define BUTTON_ALLOW 1 //login item name #define LOGIN_ITEM_NAME @"LuLu Helper" //prefs // disabled status #define PREF_IS_DISABLED @"disabled" //prefs // passive mode, & rules/action #define PREF_PASSIVE_MODE @"passiveMode" #define PREF_PASSIVE_MODE_RULES @"passiveModeRules" #define PREF_PASSIVE_MODE_ACTION @"passiveModeAction" //index of allow/block in passive mode action #define PREF_PASSIVE_MODE_ALLOW 0 #define PREF_PASSIVE_MODE_BLOCK 1 //index of no/yes in passive mode action #define PREF_PASSIVE_MODE_RULES_NO 0 #define PREF_PASSIVE_MODE_RULES_YES 1 //prefs // block mode #define PREF_BLOCK_MODE @"blockMode" //prefs // icon mode #define PREF_NO_ICON_MODE @"noIconMode" //prefs // no VT mode #define PREF_NO_VT_MODE @"noVTMode" //prefs // update mode #define PREF_NO_UPDATE_MODE @"noupdateMode" //prefs // allow all apple binaries #define PREF_ALLOW_APPLE @"allowApple" //prefs // allow all installed #define PREF_ALLOW_INSTALLED @"allowInstalled" //prefs // allow dns traffic #define PREF_ALLOW_DNS @"allowDNS" //prefs // allow localhost traffic #define PREF_ALLOW_LOCALHOST @"allowLocalHost" //prefs // allow simulator apps #define PREF_ALLOW_SIMULATOR @"allowSimulatorApps" //use global block list #define PREF_USE_BLOCK_LIST @"useBlockList" //use global allow list #define PREF_USE_ALLOW_LIST @"useAllowList" //global allow list #define PREF_ALLOW_LIST @"allowList" //global block list #define PREF_BLOCK_LIST @"blockList" //prefs // current profile #define PREF_CURRENT_PROFILE @"currentProfile" //install time // not really a 'pref' but need to save it #define PREF_INSTALL_TIMESTAMP @"installTime" //show alert options // not really a 'pref' but need to save it #define PREF_ALERT_SHOW_OPTIONS @"alertShowOptions" //rule scope // not really a 'pref' but need to save it #define PREF_ALERT_LAST_RULE_SCOPE @"alertLastRuleScope" //rule duration // not really a 'pref' but need to save it #define PREF_ALERT_LAST_RULE_DURATION @"alertLastRuleDuration" //rule duration buttons #define RULE_DURATION_BUTTON_ALWAYS 100 #define RULE_DURATION_BUTTON_PROCESS 101 #define RULE_DURATION_BUTTON_CUSTOM 102 //rule menu button #define RULE_ROW_MENU_BUTTON 110 //log file #define LOG_FILE_NAME @"LuLu.log" //error URL #define KEY_ERROR_URL @"errorURL" //flag for error popup #define KEY_ERROR_SHOULD_EXIT @"shouldExit" //general error URL #define FATAL_ERROR_URL @"https://objective-see.com/errors.html" //key for exit code #define EXIT_CODE @"exitCode" //key for error msg #define KEY_ERROR_MSG @"errorMsg" //key for error sub msg #define KEY_ERROR_SUB_MSG @"errorSubMsg" //rules changed #define RULES_CHANGED @"com.objective-see.lulu.rulesChanged" //extension event #define EXTENSION_EVENT @"com.objective-see.lulu.extensionEvent" /* INSTALLER */ //install directory #define INSTALL_DIRECTORY @"/Library/Objective-See/LuLu" //launch daemon name #define LAUNCH_DAEMON_BINARY @"LuLu" //launch daemon plist #define LAUNCH_DAEMON_PLIST @"com.objective-see.lulu.plist" //installed apps file #define INSTALLED_APPS @"installedApps" //frame shift // for status msg to avoid activity indicator #define FRAME_SHIFT 45 //button title: restart #define ACTION_RESTART @"Restart" //flag to reboot #define ACTION_RESTART_FLAG -1 //cmdline flag to uninstall #define ACTION_UNINSTALL @"-uninstall" //flag to uninstall #define ACTION_UNINSTALL_FLAG 0 //cmdline flag to uninstall #define ACTION_INSTALL @"-install" //flag to install #define ACTION_INSTALL_FLAG 1 //button title: upgrade #define ACTION_UPGRADE @"Upgrade" //button title: close #define ACTION_CLOSE @"Close" //flag to close #define ACTION_CLOSE_FLAG 2 //button title: next #define ACTION_NEXT @"Next »" //next #define ACTION_NEXT_FLAG 3 //app name #define APP_NAME @"LuLu.app" #define CMDLINE_FLAG_WELCOME @"-welcome" #define CMDLINE_FLAG_PREFS @"-prefs" #define CMDLINE_FLAG_RULES @"-rules" #define KEY_PATHS @"paths" #define KEY_RULES @"rules" #define KEY_ID @"id" #define KEY_PATH @"path" #define KEY_KEY @"key" #define KEY_CS_ID @"signatureIdentifier" #define KEY_CS_INFO @"signingInfo" #define KEY_CS_AUTHS @"signatureAuthorities" #define KEY_CS_SIGNER @"signatureSigner" #define KEY_CS_STATUS @"signatureStatus" #define KEY_CS_ENTITLEMENTS @"signatureEntitlements" #define KEY_TYPE @"type" #define KEY_SCOPE @"scope" #define KEY_ACTION @"action" #define KEY_DURATION_PROCESS @"durationProcess" #define KEY_DURATION_EXPIRATION @"durationExpiration" #define KEY_ENDPOINT_ADDR @"endpointAddr" #define KEY_ENDPOINT_PORT @"endpointPort" #define KEY_ENDPOINT_ADDR_IS_REGEX @"endpointAddrIsRegex" #define KEY_CS_CHANGE @"csChange" #define KEY_UUID @"uuid" #define KEY_PROCESS_ID @"pid" #define KEY_PROCESS_ARGS @"args" #define KEY_PROCESS_NAME @"name" #define KEY_PROCESS_PATH @"path" #define KEY_PROCESS_DELETED @"deleted" #define KEY_PROCESS_ANCESTORS @"ancestors" #define KEY_HOST @"host" #define KEY_URL @"url" #define KEY_PROTOCOL @"protocol" #define KEY_ENDPOINT @"endpoint" #define KEY_INDEX @"index" #define KEY_USER_ID @"userID" #define VALUE_ANY @"*" //keys for rule dictionary #define RULE_ID @"id" #define RULE_PATH @"path" #define RULE_HASH @"hash" #define RULE_TYPE @"type" #define RULE_USER @"user" #define RULE_ACTION @"action" #define RULE_SIGNING_INFO @"signingInfo" //rules types #define RULE_TYPE_ALL -1 #define RULE_TYPE_DEFAULT 0 #define RULE_TYPE_APPLE 1 #define RULE_TYPE_BASELINE 2 #define RULE_TYPE_USER 3 #define RULE_TYPE_PASSIVE 4 #define RULE_TYPE_RECENT 5 //rule toggle states #define RULE_TOGGLE_STATE_ENABLE 1 #define RULE_TOGGLE_STATE_DISABLE 0 //search (filter) field #define RULE_SEARCH_FIELD 5 //network monitor #define NETWORK_MONITOR @"Netiquette.app" //scope for action // from dropdown in alert window #define ACTION_SCOPE_UNSELECTED -1 #define ACTION_SCOPE_PROCESS 0 #define ACTION_SCOPE_ENDPOINT 1 //signing info (from ES) #define CS_FLAGS @"csFlags" #define PLATFORM_BINARY @"platformBinary" #define TEAM_ID @"teamID" #define SIGNING_ID @"signingID" //rules window #define WINDOW_RULES 0 //preferences window #define WINDOW_PREFERENCES 1 //key for stdout output #define STDOUT @"stdOutput" //key for stderr output #define STDERR @"stdError" //key for exit code #define EXIT_CODE @"exitCode" /* MAIN APP */ //1st welcome view #define WELCOME_VIEW_ONE 1 //2nd welcome view #define WELCOME_VIEW_TWO 2 //3rd welcome view #define WELCOME_VIEW_THREE 3 //cs consts // from: cs_blobs.h #define CS_VALID 0x00000001 #define CS_RUNTIME 0x00010000 //deactivate #define ACTION_DEACTIVATE 0 //activate #define ACTION_ACTIVATE 1 //rules menu #define MENU_RULES_VIEW 1 #define MENU_RULES_ADD 2 #define MENU_RULES_IMPORT 3 #define MENU_RULES_EXPORT 4 #define MENU_RULES_CLEANUP 5 //os major #define SUPPORTED_OS_MAJOR @"OSMajor" //os minor #define SUPPORTED_OS_MINOR @"OSMinor" //latest version #define LATEST_VERSION @"version" //updates typedef enum {Update_Error, Update_None, Update_NotSupported, Update_Available} UpdateStatus; //profiles directory #define PROFILE_DIRECTORY @"Profiles" #endif /* const_h */ ================================================ FILE: LuLu/Shared/signing.h ================================================ // // File: Signing.h // Project: Proc Info // // Created by: Patrick Wardle // Copyright: 2017 Objective-See // #ifndef Signing_h #define Signing_h @import OSLog; @import Foundation; /* FUNCTIONS */ //get the signing info of a item // audit token: extract dynamic code signing info // path specified: generate static code signing info NSMutableDictionary* extractSigningInfo(audit_token_t* token, NSString* path, SecCSFlags flags); //determine who signed item NSNumber* extractSigner(SecStaticCodeRef code, SecCSFlags flags, BOOL isDynamic); //validate a requirement OSStatus validateRequirement(SecStaticCodeRef code, SecRequirementRef requirement, SecCSFlags flags, BOOL isDynamic); //extract (names) of signing auths NSMutableArray* extractSigningAuths(NSDictionary* signingDetails); #endif ================================================ FILE: LuLu/Shared/signing.m ================================================ // // File: Signing.m // Project: LuLu // // Created by: Patrick Wardle // Copyright: 2017 Objective-See #import "consts.h" #import "signing.h" #import "utilities.h" @import Security; @import SystemConfiguration; /* GLOBALS */ //log handle extern os_log_t logHandle; //get the signing info of a item // audit token: extract dynamic code signing info // path on disk: generate static code signing info NSMutableDictionary* extractSigningInfo(audit_token_t* token, NSString* path, SecCSFlags flags) { //info dictionary NSMutableDictionary* signingInfo = nil; //status OSStatus status = !errSecSuccess; //static code ref SecStaticCodeRef staticCode = NULL; //dynamic code ref SecCodeRef dynamicCode = NULL; //signing details CFDictionaryRef signingDetails = NULL; //signing authorities NSMutableArray* signingAuths = nil; //init signing status signingInfo = [NSMutableDictionary dictionary]; //dynamic code checks // no path, dynamic check via pid if(nil == path) { //obtain dynamic code ref from (audit) token status = SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(@{(__bridge NSString *)kSecGuestAttributeAudit:[NSData dataWithBytes:token length:sizeof(audit_token_t)]}), kSecCSDefaultFlags, &dynamicCode); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCopyGuestWithAttributes' failed with %d/%#x", status, status); //set error signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:status]; //bail goto bail; } //validate code status = SecCodeCheckValidity(dynamicCode, flags, NULL); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCheckValidity' failed with %d/%#x", status, status); //set error signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:status]; //bail goto bail; } //happily signed signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:errSecSuccess]; //determine signer // apple, app store, dev id, adhoc, etc... signingInfo[KEY_CS_SIGNER] = extractSigner(dynamicCode, flags, YES); //extract signing info status = SecCodeCopySigningInformation(dynamicCode, kSecCSSigningInformation, &signingDetails); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecCodeCopySigningInformation' failed with %d/%#x", status, status); //set error signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:status]; //bail goto bail; } } //static code checks else { //create static code ref via path status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "ERROR: 'SecStaticCodeCreateWithPath' failed with %d/%#x", status, status); //set error signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:status]; //bail goto bail; } //check signature status = SecStaticCodeCheckValidity(staticCode, flags, NULL); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "'SecStaticCodeCheckValidity' failed with %d/%#x", status, status); //set error signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:status]; //bail goto bail; } //happily signed signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:errSecSuccess]; //determine signer // apple, app store, dev id, adhoc, etc... signingInfo[KEY_CS_SIGNER] = extractSigner(staticCode, flags, NO); //extract signing info status = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &signingDetails); if(errSecSuccess != status) { //err msg os_log_error(logHandle, "'SecCodeCopySigningInformation' failed with %d/%#x", status, status); //set error signingInfo[KEY_CS_STATUS] = [NSNumber numberWithInt:status]; //bail goto bail; } } //extract code signing id if(0 != [[(__bridge NSDictionary*)signingDetails objectForKey:(__bridge NSString*)kSecCodeInfoIdentifier] length]) { //extract/save signingInfo[KEY_CS_ID] = [(__bridge NSDictionary*)signingDetails objectForKey:(__bridge NSString*)kSecCodeInfoIdentifier]; } //extract signing authorities signingAuths = extractSigningAuths((__bridge NSDictionary *)(signingDetails)); if(0 != signingAuths.count) { //save signingInfo[KEY_CS_AUTHS] = signingAuths; } bail: //free signing info if(NULL != signingDetails) { //free CFRelease(signingDetails); signingDetails = NULL; } //free dynamic code if(NULL != dynamicCode) { //free CFRelease(dynamicCode); dynamicCode = NULL; } //free static code if(NULL != staticCode) { //free CFRelease(staticCode); staticCode = NULL; } return signingInfo; } //determine who signed item NSNumber* extractSigner(SecStaticCodeRef code, SecCSFlags flags, BOOL isDynamic) { //result NSNumber* signer = nil; //"anchor apple" static SecRequirementRef isApple = nil; //"anchor apple generic" static SecRequirementRef isDevID = nil; //"Apple Mac OS Application Signing" static SecRequirementRef isAppStore = nil; //"Apple iPhone OS Application Signing" static SecRequirementRef isiOSAppStore = nil; //Apple's app store team ids static NSSet* appleTeamIDs = nil; //signing details CFDictionaryRef signingDetails = NULL; //team id NSString* teamID = nil; //token static dispatch_once_t onceToken = 0; //only once // init requirements dispatch_once(&onceToken, ^{ //init apple signing requirement SecRequirementCreateWithString(CFSTR("anchor apple"), kSecCSDefaultFlags, &isApple); //init dev id signing requirement SecRequirementCreateWithString(CFSTR("anchor apple generic"), kSecCSDefaultFlags, &isDevID); //init (macOS) app store signing requirement SecRequirementCreateWithString(CFSTR("anchor apple generic and certificate leaf [subject.CN] = \"Apple Mac OS Application Signing\""), kSecCSDefaultFlags, &isAppStore); //init (iOS) app store signing requirement SecRequirementCreateWithString(CFSTR("anchor apple generic and certificate leaf [subject.CN] = \"Apple iPhone OS Application Signing\""), kSecCSDefaultFlags, &isiOSAppStore); //init Apple's App Store team IDs appleTeamIDs = [NSSet setWithArray:@[@"K36BKF7T3D", @"74J34U3R6X", @"59GAB85EFG", @"APPLECOMPUTER"]]; }); //check 1: "is apple" (proper) if(errSecSuccess == validateRequirement(code, isApple, flags, isDynamic)) { //set signer to apple signer = [NSNumber numberWithInt:Apple]; } //check 2: "is app store" // note: this is more specific than dev id, so do it first else if(errSecSuccess == validateRequirement(code, isAppStore, flags, isDynamic)) { //default signer to app store signer = [NSNumber numberWithInt:AppStore]; //however, set back to apple // ...if it's one of apple's app store apps if(errSecSuccess == SecCodeCopySigningInformation(code, kSecCSSigningInformation, &signingDetails)) { //extract team id // and check if it belongs to apple teamID = [(__bridge NSDictionary*)signingDetails objectForKey:(__bridge NSString*)kSecCodeInfoTeamIdentifier]; if( (nil != teamID) && (YES == [appleTeamIDs containsObject:teamID])) { //set signer to apple signer = [NSNumber numberWithInt:Apple]; } //release CFRelease(signingDetails); signingDetails = NULL; } } //check 3: "is (iOS) app store" // note: this is more specific than dev id, so also do it first else if(errSecSuccess == validateRequirement(code, isiOSAppStore, flags, isDynamic)) { //set signer to app store signer = [NSNumber numberWithInt:AppStore]; } //check 4: "is dev id" else if(errSecSuccess == validateRequirement(code, isDevID, flags, isDynamic)) { //set signer to dev id signer = [NSNumber numberWithInt:DevID]; } //otherwise // has to be adhoc? else { //set signer to ad hoc signer = [NSNumber numberWithInt:AdHoc]; } return signer; } //validate a requirement OSStatus validateRequirement(SecStaticCodeRef code, SecRequirementRef requirement, SecCSFlags flags, BOOL isDynamic) { //result OSStatus result = -1; //dynamic check? if(YES == isDynamic) { //validate dynamically result = SecCodeCheckValidity((SecCodeRef)code, flags, requirement); } //static check else { //validate statically result = SecStaticCodeCheckValidity(code, flags, requirement); } return result; } //extract (names) of signing auths NSMutableArray* extractSigningAuths(NSDictionary* signingDetails) { //signing auths NSMutableArray* authorities = nil; //cert chain NSArray* certificateChain = nil; //index NSUInteger index = 0; //cert SecCertificateRef certificate = NULL; //common name on chert CFStringRef commonName = NULL; //init array for certificate names authorities = [NSMutableArray array]; //get cert chain certificateChain = [signingDetails objectForKey:(__bridge NSString*)kSecCodeInfoCertificates]; if(0 == certificateChain.count) { //no certs goto bail; } //extract/save name of all certs for(index = 0; index < certificateChain.count; index++) { //reset commonName = NULL; //extract cert certificate = (__bridge SecCertificateRef)([certificateChain objectAtIndex:index]); //get common name if( (errSecSuccess == SecCertificateCopyCommonName(certificate, &commonName)) && (NULL != commonName) ) { //save [authorities addObject:(__bridge NSString*)(commonName)]; //release CFRelease(commonName); } } bail: return authorities; } ================================================ FILE: LuLu/Shared/utilities.h ================================================ // // file: utilities.h // project: lulu (shared) // description: various helper/utility functions (header) // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #ifndef Utilities_h #define Utilities_h @import AppKit; @import Foundation; /* FUNCTIONS */ //give path to bundle // get full path to its binary NSString* getBundleExecutable(NSString* appPath); //get app's version // extracted from Info.plist NSString* getAppVersion(void); //get (true) parent NSDictionary* getRealParent(pid_t pid); //get name of logged in user NSString* getConsoleUser(void); //given a path to binary // parse it back up to find app's bundle NSBundle* findAppBundle(NSString* binaryPath); //get process's path NSString* getProcessPath(pid_t pid); //get process name // either via app bundle, or path NSString* getProcessName(pid_t pid, NSString* path); //get current working dir NSString* getProcessCWD(pid_t pid); //given a process path and user // return array of all matching pids NSMutableArray* getProcessIDs(NSString* processPath, int userID); //get parent pid pid_t getParent(int pid); //enable/disable a menu void toggleMenu(NSMenu* menu, BOOL shouldEnable); //toggle login item // either add (install) or remove (uninstall) BOOL toggleLoginItem(NSURL* loginItem, int toggleFlag); //get an icon for a process // for apps, this will be app's icon, otherwise just a standard system one NSImage* getIconForProcess(NSString* path); //wait until a window is non nil // then make it modal void makeModal(NSWindowController* windowController); //find all processes by name NSMutableArray* findProcesses(NSString* processName); //hash a file (sha256) NSMutableString* hashFile(NSString* filePath); //loads a framework // note: assumes is in 'Framework' dir NSBundle* loadFramework(NSString* name); //check if (full) dark mode // meaning, Mojave+ and dark mode enabled BOOL isDarkMode(void); //check if something is nil // if so, return a default ('unknown') value NSString* valueForStringItem(NSString* item); //grab date added // extracted via 'kMDItemDateAdded' NSDate* dateAdded(NSString* file); //show an alert NSModalResponse showAlert(NSAlertStyle style, NSString* messageText, NSString* informativeText, NSArray* buttons); //get audit token for pid NSData* tokenForPid(pid_t pid); //given an ip address // reverse resolves it NSArray* resolveAddress(NSString * address); //process alive? BOOL isAlive(pid_t processID); //check if app is an simulator app // for now check 'iPhoneSimulator' and 'AppleTVSimulator' BOOL isSimulatorApp(NSString* path); //was app launched by user BOOL launchedByUser(void); //fade out a window void fadeOut(NSWindow* window, float duration); //matches CS info? BOOL matchesCSInfo(NSDictionary* csInfo_1, NSDictionary* csInfo_2); //escape string NSString* toEscapedJSON(NSString* input); //convert date to absolute NSDate* absoluteDate(NSDate* date); //generate list of ancestors NSMutableArray* generateProcessHierarchy(pid_t child); //is process on internal drive? BOOL isInternalProcess(NSString *path); #endif ================================================ FILE: LuLu/Shared/utilities.m ================================================ // // file: utilities.m // project: lulu (shared) // description: various helper/utility functions // // created by Patrick Wardle // copyright (c) 2017 Objective-See. All rights reserved. // #import "consts.h" #import "utilities.h" #import "AppDelegate.h" @import OSLog; @import Carbon; @import Security; @import Foundation; @import CommonCrypto; @import SystemConfiguration; #import #import #import #import #import #import #import #import #import /* GLOBALS */ //log handle extern os_log_t logHandle; //get app's version // extracted from Info.plist NSString* getAppVersion(void) { //read and return 'CFBundleVersion' from bundle return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; } //give path to bundle // get full path to its binary NSString* getBundleExecutable(NSString* appPath) { //binary path NSString* binaryPath = nil; //app bundle NSBundle* appBundle = nil; //load app bundle appBundle = [NSBundle bundleWithPath:appPath]; if(nil == appBundle) { //err msg os_log_error(logHandle, "ERROR: failed to load app bundle for %{public}@", appPath); //bail goto bail; } //extract executable binaryPath = [appBundle.executablePath stringByResolvingSymlinksInPath]; bail: return binaryPath; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" //get (true) parent NSDictionary* getRealParent(pid_t pid) { //process info NSDictionary* processInfo = nil; //process serial number ProcessSerialNumber psn = {0, kNoProcess}; //(parent) process serial number ProcessSerialNumber ppsn = {0, kNoProcess}; //get process serial number from pid if(noErr != GetProcessForPID(pid, &psn)) { //err goto bail; } //get process (carbon) info processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&psn, (UInt32)kProcessDictionaryIncludeAllInformationMask)); if(nil == processInfo) { //err goto bail; } //extract/convert parent ppsn ppsn.lowLongOfPSN = [processInfo[@"ParentPSN"] longLongValue] & 0x00000000FFFFFFFFLL; ppsn.highLongOfPSN = ([processInfo[@"ParentPSN"] longLongValue] >> 32) & 0x00000000FFFFFFFFLL; //get parent process (carbon) info processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&ppsn, (UInt32)kProcessDictionaryIncludeAllInformationMask)); if(nil == processInfo) { //err goto bail; } bail: return processInfo; } #pragma GCC diagnostic pop //generate list of ancestors NSMutableArray* generateProcessHierarchy(pid_t child) { //ancestors NSMutableArray* ancestors = nil; //current process id pid_t currentPID = -1; //current path NSString* currentPath = nil; //current name NSString* currentName = nil; //parent pid pid_t parentPID = -1; //rpid function static pid_t (*getRPID)(pid_t pid) = NULL; //token static dispatch_once_t onceToken = 0; //init ancestors = [NSMutableArray array]; //only once // init requirements dispatch_once(&onceToken, ^{ //get function pointer getRPID = dlsym(RTLD_NEXT, "responsibility_get_pid_responsible_for_pid"); }); //start w/ self currentPID = child; do { //get path if(nil == (currentPath = getProcessPath(currentPID))) { //default currentPath = NSLocalizedString(@"unknown", @"unknown"); } //get name currentName = getProcessName(0, currentPath); if(nil == currentName) { //default currentName = NSLocalizedString(@"unknown", @"unknown"); } //add [ancestors insertObject:[@{KEY_PROCESS_ID:[NSNumber numberWithInt:currentPID], KEY_PROCESS_PATH:currentPath, KEY_PROCESS_NAME:currentName} mutableCopy] atIndex:0]; //for apps (and if we're not root) // try application services pid via serial if(0 != getuid()) { //real parent via serial parentPID = [getRealParent(currentPID)[@"pid"] intValue]; } //not found // try via responsible pid if(0 == parentPID) { //for parent // first try via rPID if(NULL != getRPID) { //get rpid parentPID = getRPID(currentPID); } } //couldn't find/get rPID? // default back to using standard method if( (parentPID <= 0) || (currentPID == parentPID) ) { //get parent pid parentPID = getParent(currentPID); } //done? if( (parentPID <= 0) || (currentPID == parentPID) ) { //bail break; } //update currentPID = parentPID; } while(YES); //now, will all items added // add each item's index for UI purposes for(NSUInteger i = 0; i < ancestors.count; i++) { //set index ancestors[i][KEY_INDEX] = [NSNumber numberWithInteger:i]; } return ancestors; } //get name of logged in user NSString* getConsoleUser(void) { //copy/return user return CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, NULL, NULL)); } //get process name // either via app bundle, or path NSString* getProcessName(pid_t pid, NSString* path) { //status int status = -1; //process name NSString* processName = nil; //app bundle NSBundle* appBundle = nil; //buffer for process path char nameBuffer[PROC_PIDPATHINFO_MAXSIZE] = {0}; //clear memset(nameBuffer, 0x0, sizeof(nameBuffer)); //via pid? if(pid != 0) { //get name status = proc_name(pid, &nameBuffer, sizeof(nameBuffer)); if(status >= 0) { //init task's name processName = [NSString stringWithUTF8String:nameBuffer]; } } //(still) nil // try via app bundle if(nil == processName) { //find app bundle appBundle = findAppBundle(path); if(nil != appBundle) { //grab name from app's bundle processName = [appBundle infoDictionary][@"CFBundleName"]; } } //(still) nil // just grab from path if(nil == processName) { //from path processName = [path lastPathComponent]; } return processName; } //given a path to binary // parse it back up to find app's bundle NSBundle* findAppBundle(NSString* path) { //app's bundle NSBundle* appBundle = nil; //standarized path NSString* standardedPath = nil; //app's path NSString* appPath = nil; //standardize path standardedPath = [[path stringByStandardizingPath] stringByResolvingSymlinksInPath]; //first just try full path appPath = standardedPath; //try to find the app's bundle do { //try to load app's bundle appBundle = [NSBundle bundleWithPath:appPath]; //was an app passed in? if(YES == [appBundle.bundlePath isEqualToString:standardedPath]) { //all done break; } //check for match // binary path's match if( (nil != appBundle) && (YES == [appBundle.executablePath isEqualToString:standardedPath])) { //all done break; } //unset appBundle = nil; //remove last part // will try this next appPath = [appPath stringByDeletingLastPathComponent]; //scan until we get to root // of course, loop will exit if app info dictionary is found/loaded } while( (nil != appPath) && (YES != [appPath isEqualToString:@"/"]) && (YES != [appPath isEqualToString:@""]) ); return appBundle; } //get process's path NSString* getProcessPath(pid_t pid) { //task path NSString* processPath = nil; //cwd NSString* cwd = nil; //buffer for process path char pathBuffer[PROC_PIDPATHINFO_MAXSIZE] = {0}; //status int status = -1; //'management info base' array int mib[3] = {0}; //system's size for max args unsigned long systemMaxArgs = 0; //process's args char* taskArgs = NULL; //# of args int numberOfArgs = 0; //size of buffers, etc size_t size = 0; //reset buffer memset(pathBuffer, 0x0, PROC_PIDPATHINFO_MAXSIZE); //first attempt to get path via 'proc_pidpath()' status = proc_pidpath(pid, pathBuffer, sizeof(pathBuffer)); if(0 != status) { //init task's name processPath = [NSString stringWithUTF8String:pathBuffer]; } //otherwise // try via task's args ('KERN_PROCARGS2') else { //err msg os_log_error(logHandle, "ERROR: for process %d, 'proc_pidpath' failed with %d (errno: %d)", pid, status, errno); //init mib // want system's size for max args mib[0] = CTL_KERN; mib[1] = KERN_ARGMAX; //set size size = sizeof(systemMaxArgs); //get system's size for max args if(-1 == sysctl(mib, 2, &systemMaxArgs, &size, NULL, 0)) { //bail goto bail; } //alloc space for args taskArgs = malloc(systemMaxArgs); if(NULL == taskArgs) { //bail goto bail; } //init mib // want process args mib[0] = CTL_KERN; mib[1] = KERN_PROCARGS2; mib[2] = pid; //set size size = (size_t)systemMaxArgs; //get process's args if(-1 == sysctl(mib, 3, taskArgs, &size, NULL, 0)) { //bail goto bail; } //sanity check // ensure buffer is somewhat sane if(size <= sizeof(int)) { //bail goto bail; } //extract number of args memcpy(&numberOfArgs, taskArgs, sizeof(numberOfArgs)); //extract task's name // follows # of args (int) and is NULL-terminated processPath = [NSString stringWithUTF8String:taskArgs + sizeof(int)]; //short path? // get cwd + to append if(YES == [processPath hasPrefix:@"./"]) { //chop ./ processPath = [processPath substringWithRange:NSMakeRange(2, [processPath length]-2)]; cwd = getProcessCWD(pid); if(nil != cwd) { //append processPath = [cwd stringByAppendingPathComponent:processPath]; } } } bail: //free process args if(NULL != taskArgs) { //free free(taskArgs); taskArgs = NULL; } return processPath; } //get current working dir NSString* getProcessCWD(pid_t pid) { //cwd NSString* directory = nil; //status int status = -1; //path info struct proc_vnodepathinfo vpi = {0,}; //init memset(&vpi, 0x0, sizeof(vpi)); //get proc's cwd, via PROC_PIDVNODEPATHINFO status = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vpi, sizeof(vpi)); if(status > 0) { //convert to string directory = [NSString stringWithUTF8String:vpi.pvi_cdir.vip_path]; } return directory; } //given a process path and user // return array of all matching pids NSMutableArray* getProcessIDs(NSString* processPath, int userID) { //status int status = -1; //process IDs NSMutableArray* processIDs = nil; //# of procs int numberOfProcesses = 0; //array of pids pid_t* pids = NULL; //process info struct struct kinfo_proc procInfo = {0}; //size of struct size_t procInfoSize = sizeof(procInfo); //mib int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, -1}; //clear buffer memset(&procInfo, 0x0, procInfoSize); //get # of procs numberOfProcesses = proc_listallpids(NULL, 0); if(-1 == numberOfProcesses) { //bail goto bail; } //alloc buffer for pids pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t)); //alloc processIDs = [NSMutableArray array]; //get list of pids status = proc_listallpids(pids, numberOfProcesses * (int)sizeof(pid_t)); if(status < 0) { //bail goto bail; } //iterate over all pids // get name for each process for(int i = 0; i < (int)numberOfProcesses; i++) { //skip blank pids if(0 == pids[i]) { //skip continue; } //skip if path doesn't match if(YES != [processPath isEqualToString:getProcessPath(pids[i])]) { //next continue; } //need to also match on user? // caller can pass in -1 to skip this check if(-1 != userID) { //init mib mib[0x3] = pids[i]; //make syscall to get proc info for user if( (0 != sysctl(mib, 0x4, &procInfo, &procInfoSize, NULL, 0)) || (0 == procInfoSize) ) { //skip continue; } //skip if user id doesn't match if(userID != (int)procInfo.kp_eproc.e_ucred.cr_uid) { //skip continue; } } //got match // add to list [processIDs addObject:[NSNumber numberWithInt:pids[i]]]; } bail: //free buffer if(NULL != pids) { //free free(pids); //reset pids = NULL; } return processIDs; } //enable/disable a menu void toggleMenu(NSMenu* menu, BOOL shouldEnable) { //disable autoenable menu.autoenablesItems = NO; //iterate over // set state of each item for(NSMenuItem* item in menu.itemArray) { //set state item.enabled = shouldEnable; } return; } //get an icon for a process // for apps (and their helpers), this will be app's icon, otherwise just a standard system one NSImage* getIconForProcess(NSString* item) { //icon's file name NSString* iconFile = nil; //icon's path NSString* iconPath = nil; //icon's path extension NSString* iconExtension = nil; //icon NSImage* icon = nil; //system's document icon static NSImage* documentIcon = nil; //bundle NSBundle* appBundle = nil; //path // might change if we find a parent NSString* path = item; //invalid path? // grab a default icon and bail if(YES != [NSFileManager.defaultManager fileExistsAtPath:path]) { //set icon to system 'application' icon icon = [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode(kGenericApplicationIcon)]; //set size to 64 @2x [icon setSize:NSMakeSize(128, 128)]; //bail goto bail; } //helper? if( [path containsString:@"Helper"] && [path.pathExtension isEqualToString:@"app"]) { //dbg msg os_log_debug(logHandle, "%{public}@ appears to be a helper app", path); NSBundle* bundle = [NSBundle bundleWithPath:path]; NSString* iconFile = [bundle objectForInfoDictionaryKey:@"CFBundleIconFile"]; NSString* iconName = [bundle objectForInfoDictionaryKey:@"CFBundleIconName"]; //no icon? find parent if(!(iconFile.length || iconName.length)) { NSString *currentPath = path; //find parent while(currentPath.length > 1) { currentPath = [currentPath stringByDeletingLastPathComponent]; if([currentPath.pathExtension isEqualToString:@"app"] && ![currentPath containsString:@"Helper"]) { //update path = currentPath; //dbg msg os_log_debug(logHandle, "will use parents path for icon: %{public}@", path); break; } } } } //first try grab bundle // then extact icon from this appBundle = findAppBundle(path); if(nil != appBundle) { //extract icon icon = [NSWorkspace.sharedWorkspace iconForFile:appBundle.bundlePath]; if(nil != icon) { //done! goto bail; } //get file iconFile = appBundle.infoDictionary[@"CFBundleIconFile"]; //get path extension iconExtension = [iconFile pathExtension]; //if its blank (i.e. not specified) // go with 'icns' if(YES == [iconExtension isEqualTo:@""]) { //set type iconExtension = @"icns"; } //set full path iconPath = [appBundle pathForResource:[iconFile stringByDeletingPathExtension] ofType:iconExtension]; //load it icon = [[NSImage alloc] initWithContentsOfFile:iconPath]; } //process is not an app or couldn't get icon // try to get it via shared workspace if( (nil == appBundle) || (nil == icon) ) { //extract icon icon = [[NSWorkspace sharedWorkspace] iconForFile:path]; //load system document icon // static var, so only load once if(nil == documentIcon) { //load documentIcon = [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode(kGenericDocumentIcon)]; } //if 'iconForFile' method doesn't find and icon, it returns the system 'document' icon // the system 'application' icon seems more applicable, so use that here... if(YES == [icon isEqual:documentIcon]) { //set icon to system 'application' icon icon = [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode(kGenericApplicationIcon)]; } //'iconForFileType' returns small icons // so set size to 64 @2x [icon setSize:NSMakeSize(128, 128)]; } bail: return icon; } //wait till window non-nil // then make that window modal void makeModal(NSWindowController* windowController) { //window __block NSWindow* window = nil; //wait till non-nil // then make window modal for(int i=0; i<20; i++) { //grab window dispatch_sync(dispatch_get_main_queue(), ^{ //grab window = windowController.window; }); //nil? // nap if(nil == window) { //nap [NSThread sleepForTimeInterval:0.05f]; //next continue; } //have window? // make it modal dispatch_sync(dispatch_get_main_queue(), ^{ //modal [[NSApplication sharedApplication] runModalForWindow:windowController.window]; }); //done break; } return; } //find all processes by name NSMutableArray* findProcesses(NSString* processName) { //status int status = -1; //pids NSMutableArray* processes = nil; //# of procs int numberOfProcesses = 0; //array of pids pid_t* pids = NULL; //process path NSString* processPath = nil; //init processes = [NSMutableArray array]; //get # of procs numberOfProcesses = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0); if(-1 == numberOfProcesses) { //bail goto bail; } //alloc buffer for pids pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t)); //get list of pids status = proc_listpids(PROC_ALL_PIDS, 0, pids, numberOfProcesses * (int)sizeof(pid_t)); if(status < 0) { //bail goto bail; } //iterate over all pids // get name for each via helper function for(int i = 0; i < numberOfProcesses; ++i) { //skip blank pids if(0 == pids[i]) { //skip continue; } //get path processPath = getProcessPath(pids[i]); if( (nil == processPath) || (0 == processPath.length) ) { //skip continue; } //no match? if(YES != [processPath.lastPathComponent isEqualToString:processName]) { //skip continue; } //save [processes addObject:@{KEY_PROCESS_ID:[NSNumber numberWithInt:pids[i]], KEY_PATH:processPath}]; }//all procs bail: //free buffer if(NULL != pids) { //free free(pids); pids = NULL; } return processes; } //for login item enable/disable // we use the launch services APIs, since replacements don't always work :( #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" //toggle login item // either add (install) or remove (uninstall) BOOL toggleLoginItem(NSURL* loginItem, int toggleFlag) { //flag BOOL wasToggled = NO; //status OSStatus status = !noErr; //login items ref LSSharedFileListRef loginItemsRef = NULL; //login item ref LSSharedFileListItemRef loginItemRef = NULL; //login items CFArrayRef loginItems = NULL; //current login item CFURLRef currentLoginItem = NULL; //get reference to login items loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); //add (install) if(ACTION_INSTALL_FLAG == toggleFlag) { //dbg msg os_log_debug(logHandle, "adding login item: %{public}@", loginItem.path); //add loginItemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, (__bridge CFURLRef)(loginItem), NULL, NULL); if(NULL != loginItemRef) { //dbg msg os_log_debug(logHandle, "login item added"); //release CFRelease(loginItemRef); loginItemRef = NULL; } //failed else { //err msg os_log_error(logHandle, "ERROR: failed to add login item"); //bail goto bail; } //happy wasToggled = YES; } //remove (uninstall) else { //dbg msg os_log_debug(logHandle, "removing login item: %{public}@", loginItem.path); //grab all login items loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil); //iterate over all login items // look for self(s), then remove it for(id item in (__bridge NSArray *)loginItems) { //get current login item currentLoginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, NULL); if(NULL == currentLoginItem) continue; //current login item match self? if(YES == [(__bridge NSURL *)currentLoginItem isEqual:loginItem]) { //dbg msg os_log_debug(logHandle, "found match"); //remove if(noErr == (status = LSSharedFileListItemRemove(loginItemsRef, (__bridge LSSharedFileListItemRef)item))) { //nap // give some time for event to complete [NSThread sleepForTimeInterval:1.0f]; //dbg msg os_log_debug(logHandle, "removed login item"); //happy wasToggled = YES; } else { //err msg os_log_error(logHandle, "ERROR: failed to remove login item (%x)", status); //keep trying though // as might be multiple instances... } } //release CFRelease(currentLoginItem); currentLoginItem = NULL; }//all login items }//remove/uninstall bail: //release login items if(NULL != loginItems) { //release CFRelease(loginItems); loginItems = NULL; } //release login ref if(NULL != loginItemsRef) { //release CFRelease(loginItemsRef); loginItemsRef = NULL; } return wasToggled; } //grab date added // extracted via 'kMDItemDateAdded' // or if that's NULL, then 'kMDItemFSCreationDate' NSDate* dateAdded(NSString* file) { //date added NSDate* date = nil; //item MDItemRef item = NULL; //attribute names CFArrayRef attributeNames = NULL; //attributes CFDictionaryRef attributes = NULL; //app bundle NSBundle* appBundle = nil; //dbg msg os_log_debug(logHandle, "extracting 'kMDItemDateAdded' for %{public}@", file); //try find an app bundle appBundle = findAppBundle(file); if(nil != appBundle) { //init item with app's path item = MDItemCreateWithURL(NULL, (__bridge CFURLRef)appBundle.bundleURL); if(NULL == item) { goto bail; } } //no app bundle // just use item/path as it else { //init item with path item = MDItemCreateWithURL(NULL, (__bridge CFURLRef)[NSURL fileURLWithPath:file]); if(NULL == item) { goto bail; } } //get attribute names attributeNames = MDItemCopyAttributeNames(item); if(NULL == attributeNames) { goto bail; } //get attributes attributes = MDItemCopyAttributes(item, attributeNames); if(NULL == attributes) { goto bail; } //grab date added date = CFBridgingRelease(MDItemCopyAttribute(item, kMDItemDateAdded)); if(nil == date) { //dbg msg os_log_debug(logHandle, "'kMDItemDateAdded' is nil ...falling back to 'kMDItemFSCreationDate'"); //grab date via 'kMDItemFSCreationDate' date = CFBridgingRelease(MDItemCopyAttribute(item, kMDItemFSCreationDate)); } //dbg msg os_log_debug(logHandle, "extacted date, %{public}@, for %{public}@", date, file); bail: //free attributes if(NULL != attributes) { CFRelease(attributes); } //free attribute names if(NULL != attributeNames) { CFRelease(attributeNames); } //free item if(NULL != item) { CFRelease(item); } return date; } #pragma clang diagnostic pop //sha256 // as string NSMutableString* hashFile(NSString* path) { NSData* contents = [NSData dataWithContentsOfFile:path]; if (!contents) { os_log_error(logHandle, "ERROR: failed to read in %{public}@ for hashing", path); return nil; } unsigned char digest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(contents.bytes, (CC_LONG)contents.length, digest); NSMutableString* hash = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2]; for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { [hash appendFormat:@"%02x", digest[i]]; } return hash; } //get parent pid pid_t getParent(int pid) { //parent id pid_t parentID = -1; //kinfo_proc struct struct kinfo_proc processStruct; //size size_t procBufferSize = sizeof(processStruct); //mib const u_int mibLength = 4; //syscall result int sysctlResult = -1; //init mib int mib[mibLength] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; //clear buffer memset(&processStruct, 0x0, procBufferSize); //make syscall sysctlResult = sysctl(mib, mibLength, &processStruct, &procBufferSize, NULL, 0); //check if got ppid if( (noErr == sysctlResult) && (0 != procBufferSize) ) { //save ppid parentID = processStruct.kp_eproc.e_ppid; //dbg msg os_log_debug(logHandle, "extracted parent ID %d for process: %d", parentID, pid); } return parentID; } //loads a framework // note: assumes it is in 'Framework' dir NSBundle* loadFramework(NSString* name) { //handle NSBundle* framework = nil; //framework path NSString* path = nil; //init path path = [NSString stringWithFormat:@"%@/../Frameworks/%@", [NSProcessInfo.processInfo.arguments.firstObject stringByDeletingLastPathComponent], name]; //standardize path path = [path stringByStandardizingPath]; //init framework (bundle) framework = [NSBundle bundleWithPath:path]; if(NULL == framework) { //bail goto bail; } //load framework if(YES != [framework loadAndReturnError:nil]) { //bail goto bail; } bail: return framework; } //dark mode? BOOL isDarkMode(void) { //check 'AppleInterfaceStyle' return [[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"] isEqualToString:@"Dark"]; } //check if something is nil // if so, return a default ('unknown') value NSString* valueForStringItem(NSString* item) { return (nil != item) ? item : NSLocalizedString(@"unknown", @"unknown"); } //show an alert NSModalResponse showAlert(NSAlertStyle style, NSString* messageText, NSString* informativeText, NSArray* buttons) { //alert NSAlert* alert = nil; //response NSModalResponse response = 0; //init alert alert = [[NSAlert alloc] init]; //set style alert.alertStyle = style; //main text alert.messageText = messageText; //add details if(nil != informativeText) { //details alert.informativeText = informativeText; } //add buttons for(NSString* title in buttons) { //add button [alert addButtonWithTitle:title]; } //make first button, first responder alert.buttons[0].keyEquivalent = @"\r"; //center [alert.window center]; //foreground [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; //activate if(@available(macOS 14.0, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } //(re)make front [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; //show response = [alert runModal]; //(re)set activation policy [((AppDelegate*)[[NSApplication sharedApplication] delegate]) setActivationPolicy]; return response; } //get audit token for pid NSData* tokenForPid(pid_t pid) { //audit token NSData* token = nil; //task's token audit_token_t taskToken = {0}; //task task_name_t task = 0; //status kern_return_t status = !KERN_SUCCESS; //size mach_msg_type_number_t size = TASK_AUDIT_TOKEN_COUNT; //clear memset(&taskToken, 0x0, sizeof(audit_token_t)); //dbg msg os_log_debug(logHandle, "retrieving audit token for %d", pid); //get task for process status = task_name_for_pid(mach_task_self(), pid, &task); if(KERN_SUCCESS != status) { //err msg os_log_error(logHandle, "ERROR: 'task_name_for_pid' failed with %x", status); //bail goto bail; } //now get task's audit token status = task_info(task, TASK_AUDIT_TOKEN, (task_info_t)&taskToken, &size); if(KERN_SUCCESS != status) { //err msg os_log_error(logHandle, "ERROR: 'task_info' failed with %x", status); //bail goto bail; } //capture token = [NSData dataWithBytes:&taskToken length:sizeof(audit_token_t)]; //dbg msg os_log_debug(logHandle, "retrieved audit token"); bail: //cleanup task if(0 != task) { //cleanup mach_port_deallocate(mach_task_self(), task); task = 0; } return token; } //given an ip address // reverse resolves it NSArray* resolveAddress(NSString* ipAddr) { //hints struct addrinfo hints = {0}; //result struct addrinfo *result = NULL; //address CFDataRef address = {0}; //host CFHostRef host = NULL; //error CFStreamError streamError = {0}; //(resolved) host names NSArray* hostNames = nil; //dbg msg os_log_debug(logHandle, "(attempting to) reverse resolve %{public}@", ipAddr); //clear hints memset(&hints, 0x0, sizeof(hints)); //init flags hints.ai_flags = AI_NUMERICHOST; //init family hints.ai_family = PF_UNSPEC; //init type hints.ai_socktype = SOCK_STREAM; //init proto hints.ai_protocol = 0; //get addr info if(0 != getaddrinfo(ipAddr.UTF8String, NULL, &hints, &result)) { goto bail; } //convert to data address = CFDataCreate(NULL, (UInt8 *)result->ai_addr, result->ai_addrlen); if(NULL == address) { goto bail; } //create host host = CFHostCreateWithAddress(kCFAllocatorDefault, address); if(host == nil) { goto bail; } //resolve if(YES != CFHostStartInfoResolution(host, kCFHostNames, &streamError)) { goto bail; } //capture hostNames = (__bridge NSArray *)(CFHostGetNames(host, NULL)); bail: //free address if(NULL != address) { //free CFRelease(address); address = NULL; } //free host if(NULL != host) { //free CFRelease(host); host = NULL; } //free result if(NULL != result) { //free freeaddrinfo(result); result = NULL; } return hostNames; } //process alive? BOOL isAlive(pid_t processID) { //flag BOOL isAlive = YES; //reset errno errno = 0; //'management info base' array int mib[4] = {0}; //kinfo proc struct kinfo_proc procInfo = {0}; //try 'kill' with 0 // no harm done, but will fail with 'ESRCH' if process is dead kill(processID, 0); //dead proc // 'ESRCH' ->'No such process' if(ESRCH == errno) { //dead isAlive = NO; //bail goto bail; } //size size_t size = 0; //init mib mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; mib[3] = processID; //init size size = sizeof(procInfo); //get task's flags // allows to check for zombies if(0 == sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &size, NULL, 0)) { //check for zombies if(SZOMB == ((procInfo.kp_proc.p_stat) & SZOMB)) { //dead isAlive = NO; //bail goto bail; } } bail: return isAlive; } //check if app is an simulator app // for now check 'iPhoneSimulator' and 'AppleTVSimulator' BOOL isSimulatorApp(NSString* path) { //flag BOOL simulatorApp = NO; //bundle NSBundle* bundle = nil; //supported platforms NSArray* supportedPlatforms = nil; //dbg msg os_log_debug(logHandle, "checking if %{public}@ is a simulator application", path); //get bundle bundle = findAppBundle(path); if(nil == bundle) goto bail; //get supported platforms supportedPlatforms = bundle.infoDictionary[@"CFBundleSupportedPlatforms"]; if(YES != [supportedPlatforms isKindOfClass:[NSArray class]]) goto bail; //dbg msg os_log_debug(logHandle, "supported platforms: %{public}@", supportedPlatforms); //sanity check if(0 == supportedPlatforms.count) goto bail; //check if simulator app simulatorApp = [[NSSet setWithArray: supportedPlatforms] isSubsetOfSet: [NSSet setWithArray: @[@"iPhoneSimulator", @"AppleTVSimulator"]]]; bail: return simulatorApp; } //was app launched by user BOOL launchedByUser(void) { //flag BOOL byUser = NO; //parent NSDictionary* parent = nil; //get parent parent = getRealParent(getpid()); //parent dock/finder/terminal // ...then assume its user launched if( (YES == [parent[@"CFBundleIdentifier"] isEqualTo:@"com.apple.dock"]) || (YES == [parent[@"CFBundleIdentifier"] isEqualTo:@"com.apple.finder"]) || (YES == [parent[@"CFBundleIdentifier"] isEqualTo:@"com.apple.Terminal"]) ) { //set flag byUser = YES; } return byUser; } //fade out and close a window void fadeOut(NSWindow* window, float duration) { //animate fade out // and then also close [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { //set duration context.duration = duration; //set final alpha [[window animator] setAlphaValue:0.0]; } completionHandler:^{ //close [window close]; }]; return; } //matches CS info? // with some caveats (e.g. Apple App that was moved to App Store) BOOL matchesCSInfo(NSDictionary* csInfo_1, NSDictionary* csInfo_2) { //match BOOL matches = NO; //status int status_1 = -1; int status_2 = -1; //signer int signer_1 = -1; int signer_2 = -1; //signing ID NSString* signingID_1 = nil; NSString* signingID_2 = nil; //signing auths NSArray* signingAuths_1 = nil; NSArray* signingAuths_2 = nil; //extract status #1 if(nil != csInfo_1[KEY_CS_STATUS]) { //extract status_1 = [csInfo_1[KEY_CS_STATUS] intValue]; } //extract status #2 if(nil != csInfo_2[KEY_CS_STATUS]) { //extract status_2 = [csInfo_2[KEY_CS_STATUS] intValue]; } //check 0x1 // signing status mismatch? if(status_1 != status_2) { //dbg msg os_log_error(logHandle, "ERROR: code signing mismatch (signing status): %{public}@ / %{public}@", csInfo_1, csInfo_2); //bail goto bail; } //extract signer #1 if(nil != csInfo_1[KEY_CS_SIGNER]) { //extract signer_1 = [csInfo_1[KEY_CS_SIGNER] intValue]; } //extract signer #2 if(nil != csInfo_2[KEY_CS_SIGNER]) { //extract signer_2 = [csInfo_2[KEY_CS_SIGNER] intValue]; } //check 0x2 // signer mismatch? if(signer_1 != signer_2) { //but ingore apple apps that have moved if( (signer_1 == Apple && signer_2 == AppStore) || (signer_1 == AppStore && signer_2 == Apple) ) { //dbg msg os_log_error(logHandle, "ignoring case where Apple App moved to/from Mac App Store: %{public}@ / %{public}@", csInfo_1, csInfo_2); } //ok something really changed w/ signers else { //dbg msg os_log_error(logHandle, "ERROR: code signing mismatch (signer): %{public}@ / %{public}@", csInfo_1, csInfo_2); //bail goto bail; } } //extract signing ID #1 if(nil != csInfo_1[KEY_CS_ID]) { //extract signingID_1 = csInfo_1[KEY_CS_ID]; } //extract signing ID #2 if(nil != csInfo_2[KEY_CS_ID]) { //extract signingID_2 = csInfo_2[KEY_CS_ID]; } //check 0x3 // signing ID mismatch? if( ((nil != signingID_1) || (nil != signingID_2)) && (YES != [signingID_1 isEqualToString:signingID_2]) ) { //dbg msg os_log_error(logHandle, "ERROR: code signing mismatch (signing ID): %{public}@ / %{public}@", csInfo_1, csInfo_2); //bail goto bail; } //extract signing auths #1 if(nil != csInfo_1[KEY_CS_AUTHS]) { //extract signingAuths_1 = csInfo_1[KEY_CS_AUTHS]; } //extract match's signing auths #2 if(nil != csInfo_2[KEY_CS_AUTHS]) { //extract signingAuths_2 = csInfo_2[KEY_CS_AUTHS]; } //check 0x4 // signing auths mismatch? if( ((nil != signingAuths_1) || (nil != signingAuths_2)) && (YES != [signingAuths_1 isEqualToArray:signingAuths_2]) ) { //err msg os_log_error(logHandle, "ERROR: code signing mismatch (signing auths): %{public}@ / %{public}@", csInfo_1, csInfo_2); //bail goto bail; } //happy matches = YES; bail: return matches; } //escape string NSString* toEscapedJSON(NSString* input) { NSData* data = nil; NSError* error = nil; NSString* output = nil; @try { data = [NSJSONSerialization dataWithJSONObject:input options:NSJSONWritingFragmentsAllowed error:&error]; if( (nil == data) || (nil != error) ) { //err msg os_log_error(logHandle, "ERROR: failed to convert/escape %{public}@ to JSON (error: %{public}@)", input, error); goto bail; } } @catch(NSException* exception) { //err msg os_log_error(logHandle, "ERROR: failed to convert/escape %{public}@ to JSON (exception: %{public}@)", input, exception); goto bail; } output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; bail: return output; } //is process on internal drive? BOOL isInternalProcess(NSString *path) { NSError* error = nil; NSNumber* isInternal = nil; if(YES != [[NSURL fileURLWithPath:path] getResourceValue:&isInternal forKey:NSURLVolumeIsInternalKey error:&error] || (nil != error)) { //err msg os_log_error(logHandle, "ERROR: 'getResourceValue'/'NSURLVolumeIsInternalKey' failed with %@", error); goto bail; } bail: return isInternal.boolValue; } ================================================ FILE: LuLu/Tests/README.md ================================================ # LuLu Passive Mode Improvements Tests This directory contains tests for the passive mode FQDN rule creation improvements. ## What's Tested ### 🎯 Domain Name Prioritization - Prioritizes `flow.URL.host` over `flow.remoteHostname` over `remoteEndpoint.hostname` - Ensures rules use domain names like `github.com` instead of IP addresses like `140.82.112.3` ### 🎨 Smart Port Display - Hides common ports (80, 443) for cleaner UI display - Shows uncommon ports (8080, 3000, etc.) to highlight important information - Preserves full data internally for precise filtering ### 🔗 End-to-End Integration - Validates complete flow from network traffic to final rule display - Tests real-world scenarios with complex URLs and various port configurations ## Running Tests ```bash # Run the complete test suite ./run_passive_mode_tests.sh ``` ## Test Results ✅ **9/9 tests pass** covering all functionality ### Before/After Examples | Before (IP-based) | After (Domain-based) | |------------------|---------------------| | `140.82.112.3:443` | `github.com` | | `52.36.184.210:443` | `api.slack.com` | | `127.0.0.1:8080` | `localhost:8080` | ## Files - `test_passive_mode_improvements.m` - Comprehensive test suite - `run_passive_mode_tests.sh` - Build and run script - `README.md` - This file ================================================ FILE: LuLu/Tests/run_passive_mode_tests.sh ================================================ #!/bin/bash # # run_passive_mode_tests.sh # Script to compile and run passive mode improvements tests # echo "🚀 Building and running passive mode improvements tests..." echo "========================================================" # Set up paths SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEST_FILE="$SCRIPT_DIR/test_passive_mode_improvements.m" TEST_BINARY="$SCRIPT_DIR/test_passive_mode_improvements" # Check if test file exists if [ ! -f "$TEST_FILE" ]; then echo "❌ Error: Test file not found at $TEST_FILE" exit 1 fi echo "📁 Test directory: $SCRIPT_DIR" echo "📄 Test file: $TEST_FILE" # Compile the test echo "" echo "🔨 Compiling test..." clang -framework Foundation -framework NetworkExtension \ -o "$TEST_BINARY" \ "$TEST_FILE" \ -Wno-objc-missing-property-synthesis \ -Wno-incomplete-implementation # Check if compilation succeeded if [ $? -ne 0 ]; then echo "❌ Compilation failed!" exit 1 fi echo "✅ Compilation successful!" # Run the test echo "" echo "🧪 Running tests..." echo "==================" "$TEST_BINARY" # Capture test result TEST_RESULT=$? # Clean up echo "" echo "🧹 Cleaning up..." rm -f "$TEST_BINARY" # Report final result if [ $TEST_RESULT -eq 0 ]; then echo "✅ All tests completed successfully!" else echo "❌ Tests failed with exit code $TEST_RESULT" fi exit $TEST_RESULT ================================================ FILE: LuLu/Tests/test_passive_mode_improvements.m ================================================ // // test_complete_functionality.m // LuLu // // Complete test suite for hostname prioritization and port display functionality // #import #import // Import our constants #define VALUE_ANY @"*" // Mock FilterDataProvider interface for testing @interface TestFilterDataProvider : NSObject - (NSString*)getBestHostnameFromFlow:(NEFilterSocketFlow*)flow; @end @implementation TestFilterDataProvider // Copy of our implementation for testing - (NSString*)getBestHostnameFromFlow:(NEFilterSocketFlow*)flow { //best hostname NSString* bestHostname = nil; //remote endpoint NWHostEndpoint* remoteEndpoint = nil; //extract remote endpoint remoteEndpoint = (NWHostEndpoint*)flow.remoteEndpoint; //priority 1: try flow.URL.host (best for domain names) if(nil != flow.URL.host && 0 != flow.URL.host.length) { bestHostname = flow.URL.host; goto bail; } //priority 2: try flow.remoteHostname (macOS 11+) if(@available(macOS 11, *)) { if(nil != flow.remoteHostname && 0 != flow.remoteHostname.length) { bestHostname = flow.remoteHostname; goto bail; } } //priority 3: fallback to remoteEndpoint.hostname (may be IP address) if(nil != remoteEndpoint.hostname && 0 != remoteEndpoint.hostname.length) { bestHostname = remoteEndpoint.hostname; } bail: return bestHostname; } @end // Mock Rule class for testing port display @interface TestRule : NSObject @property (nonatomic, strong) NSString* endpointAddr; @property (nonatomic, strong) NSString* endpointPort; @end @implementation TestRule @synthesize endpointAddr, endpointPort; // Port display logic (copied from RulesWindowController.m) - (NSString*)displayString { NSString* address = [self.endpointAddr isEqualToString:VALUE_ANY] ? @"any address" : self.endpointAddr; NSString* port = [self.endpointPort isEqualToString:VALUE_ANY] ? @"any port" : self.endpointPort; // Smart port display (hide common ports 80, 443) if (([self.endpointPort isEqualToString:@"80"] || [self.endpointPort isEqualToString:@"443"]) && NO == [self.endpointAddr isEqualToString:VALUE_ANY] && NO == [self.endpointPort isEqualToString:VALUE_ANY]) { // Hide common ports for cleaner display return address; } else { // Show port for uncommon ports or when using "any" values return [NSString stringWithFormat:@"%@:%@", address, port]; } } @end // Test helper to create simple flow objects for testing @interface TestFlow : NEFilterSocketFlow @property (nonatomic, strong) NSURL* URL; @property (nonatomic, strong) NSString* remoteHostname; @property (nonatomic, strong) NWHostEndpoint* remoteEndpoint; @end @implementation TestFlow @synthesize URL, remoteHostname, remoteEndpoint; @end int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"🧪 Complete Functionality Test Suite"); NSLog(@"==================================="); TestFilterDataProvider* provider = [[TestFilterDataProvider alloc] init]; int testsPassed = 0; int totalTests = 0; NSLog(@"\n🔍 PART 1: Hostname Prioritization Tests"); NSLog(@"========================================"); // Test 1: URL host prioritization { totalTests++; NSLog(@"\n📋 Test 1: URL host prioritization"); TestFlow* flow = [[TestFlow alloc] init]; flow.URL = [NSURL URLWithString:@"https://github.com/"]; flow.remoteEndpoint = [NWHostEndpoint endpointWithHostname:@"140.82.112.3" port:@"443"]; NSString* result = [provider getBestHostnameFromFlow:flow]; if ([result isEqualToString:@"github.com"]) { NSLog(@"✅ PASS: Got '%@' (expected domain name over IP)", result); testsPassed++; } else { NSLog(@"❌ FAIL: Got '%@' (expected 'github.com')", result); } } // Test 2: Remote hostname fallback { totalTests++; NSLog(@"\n📋 Test 2: Remote hostname fallback"); TestFlow* flow = [[TestFlow alloc] init]; flow.remoteHostname = @"api.github.com"; flow.remoteEndpoint = [NWHostEndpoint endpointWithHostname:@"140.82.112.3" port:@"443"]; NSString* result = [provider getBestHostnameFromFlow:flow]; if ([result isEqualToString:@"api.github.com"]) { NSLog(@"✅ PASS: Got '%@' (expected remote hostname)", result); testsPassed++; } else { NSLog(@"❌ FAIL: Got '%@' (expected 'api.github.com')", result); } } NSLog(@"\n🎨 PART 2: Port Display Tests"); NSLog(@"============================="); // Test 3: Hide port 443 for HTTPS { totalTests++; NSLog(@"\n📋 Test 3: Hide port 443 for HTTPS"); TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = @"github.com"; rule.endpointPort = @"443"; NSString* display = [rule displayString]; if ([display isEqualToString:@"github.com"]) { NSLog(@"✅ PASS: Display '%@' (hidden port 443)", display); testsPassed++; } else { NSLog(@"❌ FAIL: Display '%@' (expected 'github.com')", display); } } // Test 4: Hide port 80 for HTTP { totalTests++; NSLog(@"\n📋 Test 4: Hide port 80 for HTTP"); TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = @"example.com"; rule.endpointPort = @"80"; NSString* display = [rule displayString]; if ([display isEqualToString:@"example.com"]) { NSLog(@"✅ PASS: Display '%@' (hidden port 80)", display); testsPassed++; } else { NSLog(@"❌ FAIL: Display '%@' (expected 'example.com')", display); } } // Test 5: Show uncommon port 8080 { totalTests++; NSLog(@"\n📋 Test 5: Show uncommon port 8080"); TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = @"localhost"; rule.endpointPort = @"8080"; NSString* display = [rule displayString]; if ([display isEqualToString:@"localhost:8080"]) { NSLog(@"✅ PASS: Display '%@' (shown uncommon port)", display); testsPassed++; } else { NSLog(@"❌ FAIL: Display '%@' (expected 'localhost:8080')", display); } } // Test 6: Show port when address is "any" { totalTests++; NSLog(@"\n📋 Test 6: Show port when address is 'any'"); TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = VALUE_ANY; rule.endpointPort = @"443"; NSString* display = [rule displayString]; if ([display isEqualToString:@"any address:443"]) { NSLog(@"✅ PASS: Display '%@' (shown port for 'any' address)", display); testsPassed++; } else { NSLog(@"❌ FAIL: Display '%@' (expected 'any address:443')", display); } } // Test 7: Show port when port is "any" { totalTests++; NSLog(@"\n📋 Test 7: Show port when port is 'any'"); TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = @"github.com"; rule.endpointPort = VALUE_ANY; NSString* display = [rule displayString]; if ([display isEqualToString:@"github.com:any port"]) { NSLog(@"✅ PASS: Display '%@' (shown 'any port')", display); testsPassed++; } else { NSLog(@"❌ FAIL: Display '%@' (expected 'github.com:any port')", display); } } NSLog(@"\n🔗 PART 3: Integration Tests"); NSLog(@"============================"); // Test 8: End-to-end domain + port hiding { totalTests++; NSLog(@"\n📋 Test 8: End-to-end: domain extraction + port hiding"); // Simulate the full flow: extract hostname from flow, create rule, display TestFlow* flow = [[TestFlow alloc] init]; flow.URL = [NSURL URLWithString:@"https://api.slack.com/api/conversations.list"]; flow.remoteEndpoint = [NWHostEndpoint endpointWithHostname:@"52.36.184.210" port:@"443"]; NSString* hostname = [provider getBestHostnameFromFlow:flow]; NSString* port = flow.remoteEndpoint.port; TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = hostname; rule.endpointPort = port; NSString* display = [rule displayString]; if ([display isEqualToString:@"api.slack.com"]) { NSLog(@"✅ PASS: Full flow '%@' (domain extracted, port hidden)", display); NSLog(@" • Original: https://api.slack.com/api/conversations.list → 52.36.184.210:443"); NSLog(@" • Improved: %@", display); testsPassed++; } else { NSLog(@"❌ FAIL: Full flow '%@' (expected 'api.slack.com')", display); } } // Test 9: Custom port preserved in full flow { totalTests++; NSLog(@"\n📋 Test 9: End-to-end: domain extraction + custom port shown"); TestFlow* flow = [[TestFlow alloc] init]; flow.URL = [NSURL URLWithString:@"http://localhost:3000/api"]; flow.remoteEndpoint = [NWHostEndpoint endpointWithHostname:@"127.0.0.1" port:@"3000"]; NSString* hostname = [provider getBestHostnameFromFlow:flow]; NSString* port = flow.remoteEndpoint.port; TestRule* rule = [[TestRule alloc] init]; rule.endpointAddr = hostname; rule.endpointPort = port; NSString* display = [rule displayString]; if ([display isEqualToString:@"localhost:3000"]) { NSLog(@"✅ PASS: Full flow '%@' (domain extracted, custom port shown)", display); NSLog(@" • Original: http://localhost:3000/api → 127.0.0.1:3000"); NSLog(@" • Improved: %@", display); testsPassed++; } else { NSLog(@"❌ FAIL: Full flow '%@' (expected 'localhost:3000')", display); } } // Test Results Summary NSLog(@"\n🏁 Complete Test Results"); NSLog(@"========================"); NSLog(@"Tests Passed: %d/%d", testsPassed, totalTests); if (testsPassed == totalTests) { NSLog(@"✅ ALL TESTS PASSED!"); NSLog(@""); NSLog(@"📊 Feature Summary:"); NSLog(@" ✅ Domain name prioritization working"); NSLog(@" ✅ Port hiding for common ports (80, 443)"); NSLog(@" ✅ Port showing for uncommon ports"); NSLog(@" ✅ End-to-end functionality verified"); NSLog(@""); NSLog(@"🎯 Before/After Examples:"); NSLog(@" Old: 140.82.112.3:443 → New: github.com"); NSLog(@" Old: 52.36.184.210:443 → New: api.slack.com"); NSLog(@" Old: 127.0.0.1:8080 → New: localhost:8080"); return 0; } else { NSLog(@"❌ %d tests failed. Please check implementation.", totalTests - testsPassed); return 1; } } } ================================================ FILE: README.md ================================================ # LuLu [简体中文](README_zh-Hans.md) | [正體中文](README_zh-Hant.md) LuLu is the free open-source macOS firewall:

**Documentation:** \ Full details and usage instructions can be found [here](https://objective-see.com/products/lulu.html). **To Support:** \ ❤  Love this product and want to support it? Please check out my [patreon page](https://www.patreon.com/objective_see).

================================================ FILE: README_zh-Hans.md ================================================ # LuLu [English](README.md) | [正體中文](README_zh-Hant.md) LuLu 是免费的开源 macOS 防火墙:

**文档:** \ 可[在此处](https://objective-see.com/products/lulu.html)找到完整的详细信息和使用说明。 **支持:** \ ❤  喜欢这个产品并想支持它?请查看我的 [patreon 页面](https://www.patreon.com/objective_see).

================================================ FILE: README_zh-Hant.md ================================================ # LuLu [English](README.md) | [简体中文](README_zh-Hans.md) LuLu 是免費的開源 macOS 防火牆:

** 文件:** \ 可[在此處](https://objective-see.com/products/lulu.html)找到完整的詳細資訊和使用説明。 **支持:** \ ❤  喜歡這個産品並想支持它?請查看我的 [patreon 頁面](https://www.patreon.com/objective_see).

================================================ FILE: lulu.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: lulu.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning